using System;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using System.Windows.Forms;
using Vanara.InteropServices;
using Vanara.PInvoke;
using static Vanara.PInvoke.Ole32;
using static Vanara.PInvoke.Shell32;

namespace Vanara.Windows.Shell
{
	/// <summary>Constants used to indicate how a file in use is being used.</summary>
	/// <remarks>
	/// The interpretation of "playing" or "editing" is left to the application's implementation. Generally, "playing" would refer to a
	/// media file while "editing" can refer to any file being altered in an application. However, the application itself best knows how to
	/// map these terms to its actions.
	/// </remarks>
	public enum FileUsageType
	{
		/// <summary>The file is being played by the process that has it open.</summary>
		Playing = FILE_USAGE_TYPE.FUT_PLAYING,

		/// <summary>The file is being edited by the process that has it open.</summary>
		Editing = FILE_USAGE_TYPE.FUT_EDITING,

		/// <summary>
		/// The file is open in the process for an unspecified action or an action that does not readily fit into the other two categories.
		/// </summary>
		Generic = FILE_USAGE_TYPE.FUT_GENERIC,
	}

	/// <summary>
	/// A handler for applications that open file types that can be opened by other applications. An application's use of this object
	/// enables Windows Explorer to discover the source of sharing errors, which enables users to address and retry operations that fail due
	/// to those errors. This object handles registering the file with the Running Object Table (see <see cref="Ole32.IRunningObjectTable" />). It will revoke
	/// that registration on disposal or when the <see cref="Registered" /> property is set to <see langword="false"/>.
	/// </summary>
	/// <remarks>This object should be created for the duration of use of any file that can be opened by other applications. It's scope should follow
	/// that of the file handle or file stream.</remarks>
	/// <example>
	///   <code title="Example use of FileInUseHandler"><![CDATA[// The following should be taken as example methods within the application that handle opening and closing files.
	///private Dictionary<string, (FileStream fileStream, FileInUseHandler inUseHandler)> openFiles =
	///   new Dictionary<string, (FileStream, FileInUseHandler)>();
	///
	///private void OpenFile(string filePath)
	///{
	///	// Create the in-use handler tied to the 'mainForm' of this application.
	///	var inUseHandler = new FileInUseHandler(filePath, mainForm, FileUsageType.Editing);
	///	// Assign event handler to close this file if requested and allowed.
	///	inUseHandler.CloseFile += (s, e) => CloseFile(((FileInUseHandler)s).FilePath);
	///	// Assign event handler to prompt the user if another app requests control of this file.
	///	inUseHandler.CloseRequested += (s, e) =>
	///	   e.Cancel = MessageBox.Show($"Another application has requested that {Path.GetFileName(((FileInUseHandler)s).FilePath)} be closed to allow it to edit. Allow?",
	///	   "Close file request", MessageBoxButtons.YesNo) == DialogResult.No;
	///	// Capture file name, open file stream and in-use handler.
	///	openFiles.Add(filePath, (File.OpenWrite(filePath), inUseHandler));
	///}
	///
	///private void CloseFile(string filePath)
	///{
	///	// If the file is open, close the stream via disposal, and revoke in-use registration via disposal
	///	if (openFiles.TryGetValue(filePath, out var info))
	///	{
	///		info.fileStream.Dispose();
	///		info.inUseHandler.Dispose();
	///		openFiles.Remove(filePath);
	///	}
	///}]]></code>
	/// </example>
	/// <seealso cref="IFileIsInUse" />
	/// <seealso cref="IDisposable" />
	public class FileInUseHandler : IFileIsInUse, IDisposable
	{
		private bool disposedValue;
		private string filePath;
		private IMoniker moniker;
		private uint regId;

		/// <summary>Initializes a new instance of the <see cref="FileInUseHandler"/> class.</summary>
		/// <param name="filePath">The file path.</param>
		/// <param name="parent">The parent.</param>
		/// <param name="usageType">Type of the usage.</param>
		public FileInUseHandler(string filePath, IWin32Window parent = null, FileUsageType usageType = FileUsageType.Generic)
		{
			ActivationWindow = parent;
			FileUsageType = usageType;
			FilePath = filePath;
		}

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

		/// <summary>Occurs after permission has been given to close the file and the file must now be closed.</summary>
		public event EventHandler CloseFile;

		/// <summary>
		/// Occurs when another application is requesting that the file be closed. If this event is not registered or if in response to this
		/// event, you set the <see cref="CancelEventArgs.Cancel"/> property to <see langword="true"/>, then the requesting application will
		/// be informed that it cannot take control.
		/// </summary>
		public event CancelEventHandler CloseRequested;

		/// <summary>
		/// Gets or sets the top-level window of the application that is using the file that should be activated when requested. If this
		/// value is <see langword="null"/>, then the calling application will be told that it cannot activate the file's owning application.
		/// </summary>
		/// <value>The activation window.</value>
		public IWin32Window ActivationWindow { get; set; }

		/// <summary>
		/// Gets or sets the full path to the file that is in use by this application. Setting this value will revoke any prior file's
		/// registration in the ROT and register this file there.
		/// </summary>
		/// <value>The full path to the file that is in use by this application. This value cannot be <see langword="null"/>.</value>
		/// <exception cref="ArgumentNullException">FilePath</exception>
		public string FilePath
		{
			get => filePath;
			set
			{
				RevokeFromROT();
				filePath = value ?? throw new ArgumentNullException(nameof(FilePath));
				RegisterInROT();
			}
		}

		/// <summary>Gets or sets a value that indicates how the file in use is being used.</summary>
		/// <value>The type of the file use.</value>
		public FileUsageType FileUsageType { get; set; }

		/// <summary>
		/// Gets or sets a value indicating whether the file specified in <see cref="FilePath"/> is registered in the Running Object Table.
		/// </summary>
		/// <value><see langword="true"/> if registered; otherwise, <see langword="false"/>.</value>
		public bool Registered
		{
			get => regId != 0;
			set
			{
				if (!value)
					RevokeFromROT();
				else if (regId == 0)
					RegisterInROT();
			}
		}

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

		HRESULT IFileIsInUse.CloseFile()
		{
			if (CloseFile is null) return HRESULT.E_NOTIMPL;
			CloseFile.Invoke(this, EventArgs.Empty);
			RevokeFromROT();
			return HRESULT.S_OK;
		}

		HRESULT IFileIsInUse.GetAppName(out string ppszName)
		{
			ppszName = System.Windows.Forms.Application.ProductName;
			return HRESULT.S_OK;
		}

		HRESULT IFileIsInUse.GetCapabilities(out OF_CAP pdwCapFlags)
		{
			var cancelArgs = new CancelEventArgs(false);
			CloseRequested?.Invoke(this, cancelArgs);
			pdwCapFlags = (!cancelArgs.Cancel ? OF_CAP.OF_CAP_CANCLOSE : 0) | (ActivationWindow is null ? 0 : OF_CAP.OF_CAP_CANSWITCHTO);
			return HRESULT.S_OK;
		}

		HRESULT IFileIsInUse.GetSwitchToHWND(out HWND phwnd)
		{
			phwnd = ActivationWindow is null ? default : ActivationWindow.Handle;
			return HRESULT.S_OK;
		}

		HRESULT IFileIsInUse.GetUsage(out FILE_USAGE_TYPE pfut)
		{
			pfut = (FILE_USAGE_TYPE)FileUsageType;
			return HRESULT.S_OK;
		}

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

				disposedValue = true;
			}
		}

		private void RegisterInROT()
		{
			GetRunningObjectTable(0, out var rot).ThrowIfFailed();
			using var prot = ComReleaserFactory.Create(rot);
			CreateFileMoniker(FilePath, out moniker).ThrowIfFailed();
			regId = rot.Register(ROTFLAGS.ROTFLAGS_REGISTRATIONKEEPSALIVE | ROTFLAGS.ROTFLAGS_ALLOWANYCLIENT, (object)(IFileIsInUse)this, moniker);
		}

		private void RevokeFromROT()
		{
			if (regId == 0) return;
			GetRunningObjectTable(0, out var rot).ThrowIfFailed();
			using var prot = ComReleaserFactory.Create(rot);
			try { rot.Revoke(regId); } catch { }
			regId = 0;
			Marshal.FinalReleaseComObject(moniker);
			moniker = null;
		}
	}
}