using static Vanara.PInvoke.Gdi32;
using static Vanara.PInvoke.User32;

namespace Vanara.PInvoke;

/// <summary>Interface identifying a class that can subclass a window proceedure.</summary>
public interface IWindowInit
{
	/// <summary>
	/// Method called on WM_NCCREATE which takes control of the window procedure. The window must provide a GCHandle pointer to itself in
	/// the lpParam parameter of CreateWindowEx. This method implentation should confirm it's HWND value against <paramref name="hwnd"/>
	/// and then using SetWindowLongPtr with GWLP_WNDPROC to update the window procedure.
	/// </summary>
	/// <param name="hwnd">A handle to the window.</param>
	/// <param name="msg">Always WM_NCCREATE.</param>
	/// <param name="wParam">A pointer to the window class' WindowProc delegate.</param>
	/// <param name="lParam">A pointer to a <see cref="CREATESTRUCT"/> instance with the window creation paramters.</param>
	/// <returns>The return should be IntPtr(1) to indicate success. Any other value will stop the completion of CreateWindowEx.</returns>
	IntPtr InitWndProcOnNCCreate(HWND hwnd, uint msg, IntPtr wParam, IntPtr lParam);
}

/// <summary>Encapsulates a window class.</summary>
public class WindowClass
{
	/// <summary>The instance of the <see cref="WNDCLASSEX"/> populated for the window class.</summary>
	public readonly WNDCLASSEX wc;

	private static readonly uint cbSize = (uint)Marshal.SizeOf(typeof(WNDCLASSEX));
	private static SafeHICON? appIcon;
	private static SafeHCURSOR? arrowCursor;
	private readonly WindowProc instProc;
	private readonly WindowProc? wndProc;

	/// <summary>Initializes a new instance of the <see cref="WindowClass"/> class and registers the class name.</summary>
	/// <param name="className">
	/// <para>
	/// A string that specifies the window class name. The class name can be any name registered with RegisterClass or RegisterClassEx,
	/// or any of the predefined control-class names.
	/// </para>
	/// <para>
	/// The maximum length for <c>lpszClassName</c> is 256. If <c>lpszClassName</c> is greater than the maximum length, the
	/// RegisterClassEx function will fail.
	/// </para>
	/// </param>
	/// <param name="hInst">A handle to the instance that contains the window procedure for the class.</param>
	/// <param name="wndProc">
	/// A pointer to the window procedure. You must use the CallWindowProc function to call the window procedure. For more information,
	/// see WindowProc.
	/// </param>
	/// <param name="styles">The class style(s). This member can be any combination of the Class Styles.</param>
	/// <param name="hIcon">
	/// A handle to the class icon. This member must be a handle to an icon resource. If this member is <c>NULL</c>, the system provides
	/// a default icon.
	/// </param>
	/// <param name="hSmIcon">
	/// A handle to a small icon that is associated with the window class. If this member is <c>NULL</c>, the system searches the icon
	/// resource specified by the <c>hIcon</c> member for an icon of the appropriate size to use as the small icon.
	/// </param>
	/// <param name="hCursor">
	/// A handle to the class cursor. This member must be a handle to a cursor resource. If this member is <c>NULL</c>, an application
	/// must explicitly set the cursor shape whenever the mouse moves into the application's window.
	/// </param>
	/// <param name="hbrBkgd">
	/// A handle to the class background brush. This member can be a handle to the brush to be used for painting the background, or it
	/// can be a color value. A color value must be one of the following standard system colors (the value 1 must be added to the chosen color).
	/// <para>
	/// The system automatically deletes class background brushes when the class is unregistered by using <see cref="UnregisterClass"/>.
	/// An application should not delete these brushes.
	/// </para>
	/// <para>
	/// When this member is <c>NULL</c>, an application must paint its own background whenever it is requested to paint in its client
	/// area. To determine whether the background must be painted, an application can either process the WM_ERASEBKGND message or test
	/// the <c>fErase</c> member of the PAINTSTRUCT structure filled by the BeginPaint function.
	/// </para>
	/// </param>
	/// <param name="menuName">
	/// A string that specifies the resource name of the class menu, as the name appears in the resource file. If you use an integer to
	/// identify the menu, use the MAKEINTRESOURCE macro. If this member is <c>NULL</c>, windows belonging to this class have no default menu.
	/// </param>
	/// <param name="extraBytes">
	/// The number of extra bytes to allocate following the window-class structure. The system initializes the bytes to zero.
	/// </param>
	/// <param name="extraWinBytes">
	/// The number of extra bytes to allocate following the window instance. The system initializes the bytes to zero. If an application
	/// uses <c>WNDCLASSEX</c> to register a dialog box created by using the <c>CLASS</c> directive in the resource file, it must set
	/// this member to <c>DLGWINDOWEXTRA</c>.
	/// </param>
	public WindowClass(string? className = null, HINSTANCE hInst = default, WindowProc? wndProc = default, WindowClassStyles styles = 0, HICON hIcon = default, HICON hSmIcon = default,
		HCURSOR hCursor = default, HBRUSH hbrBkgd = default, string? menuName = null, int extraBytes = 0, int extraWinBytes = 0) : this()
	{
		this.wndProc = wndProc ?? DefWindowProc;
		wc = new WNDCLASSEX
		{
			cbSize = cbSize,
			lpfnWndProc = instProc,
			hInstance = hInst.IsNull ? Kernel32.GetModuleHandle() : hInst,
			lpszClassName = className ?? Guid.NewGuid().ToString("N"),
			style = styles,
			hIcon = hIcon,
			hIconSm = hSmIcon,
			hCursor = hCursor,
			hbrBackground = hbrBkgd,
			lpszMenuName = menuName,
			cbClsExtra = extraBytes,
			cbWndExtra = extraWinBytes,
		};
		Win32Error.ThrowLastErrorIfFalse(!RegisterClassEx(wc).IsInvalid);
	}

	/// <summary>Initializes a new instance of the <see cref="WindowClass"/> class using a <see cref="WNDCLASSEX"/> instance.</summary>
	/// <param name="wcx">The <see cref="WNDCLASSEX"/> instance.</param>
	/// <param name="register">if set to <see langword="true"/>, <paramref name="wcx"/> is used to register the class.</param>
	public WindowClass(in WNDCLASSEX wcx, bool register = true) : this()
	{
		wc = wcx;
		if (register)
		{
			wndProc = wc.lpfnWndProc ?? DefWindowProc;
			wc.lpfnWndProc = instProc;
			Win32Error.ThrowLastErrorIfFalse(!RegisterClassEx(wc).IsInvalid);
		}
	}

	private WindowClass() => instProc = PrimaryClassWndProc;

	/// <summary>Gets a handle to the brush representing the standard MDI window background (COLOR_APPWORKSPACE).</summary>
	/// <value>The standard MDI window background brush handle.</value>
	public static HBRUSH MdiWindowBrush => (HBRUSH)(IntPtr)(SystemColorIndex.COLOR_APPWORKSPACE + 1);

	/// <summary>
	/// Gets a handle to a null brush (GetStockObject(NULL_BRUSH)). Use this for the background of non-displayable windows or to prevent
	/// flicker on custom drawn backgrounds.
	/// </summary>
	/// <value>The null background brush handle.</value>
	public static HBRUSH NullBrush => GetStockObject(StockObjectType.NULL_BRUSH);

	/// <summary>Gets a handle to the standard application icon (IDI_APPLICATION).</summary>
	/// <value>The standard application icon handle.</value>
	public static HICON StdAppIcon => appIcon ??= LoadIcon(default, IDI_APPLICATION);

	/// <summary>Gets a handle to the standard arrow cursor (IDC_ARROW).</summary>
	/// <value>The standard arrow cursor handle.</value>
	public static HCURSOR StdArrowCursor => arrowCursor ??= LoadCursor(default, IDC_ARROW);

	/// <summary>Gets a handle to the brush representing the standard window background (COLOR_WINDOW).</summary>
	/// <value>The standard window background brush handle.</value>
	public static HBRUSH WindowBrush => (HBRUSH)(IntPtr)(SystemColorIndex.COLOR_WINDOW + 1);

	/// <summary>Gets the windows class name.</summary>
	public string ClassName => wc.lpszClassName;

	/// <summary>Gets the class style(s).</summary>
	public WindowClassStyles Styles => wc.style;

	/// <summary>Gets the <see cref="WindowProc"/> that is executed by this class.</summary>
	/// <value>The executing <see cref="WindowProc"/>.</value>
	public WindowProc WndProc => wndProc ?? DefWindowProc;

	/// <summary>Gets a <see cref="WindowClass"/> instance associated with a window handle.</summary>
	/// <param name="hWnd">The window handle to examine.</param>
	/// <returns>
	/// If the function finds a matching window and successfully copies the data, the return value is a <see cref="WindowClass"/>
	/// instance. If not, <see langword="null"/> is returned.
	/// </returns>
	public static WindowClass? GetInstanceFromWindow(HWND hWnd) => GetNamedInstance(GetClassName(hWnd), GetClassLong(hWnd, GetClassLongFlag.GCL_HMODULE));

	/// <summary>Gets a <see cref="WindowClass"/> instance by looking up the name.</summary>
	/// <param name="className">
	/// The class name. The name must be that of a preregistered class or a class registered by a previous call to the RegisterClass or
	/// RegisterClassEx function.
	/// </param>
	/// <param name="hInst">
	/// A handle to the instance of the application that created the class. To retrieve information about classes defined by the system
	/// (such as buttons or list boxes), set this parameter to NULL.
	/// </param>
	/// <returns>
	/// If the function finds a matching class and successfully copies the data, the return value is a <see cref="WindowClass"/>
	/// instance. If not, <see langword="null"/> is returned.
	/// </returns>
	public static WindowClass? GetNamedInstance(string className, HINSTANCE hInst)
	{
		if (string.IsNullOrEmpty(className))
			throw new ArgumentException($"'{nameof(className)}' cannot be null or empty.", nameof(className));

		return GetClassInfoEx(hInst, className, out var wcx) ? new(wcx, false) : null;
	}

	/// <summary>Creates a <see cref="WindowClass"/> instance that uses common settings for a displayed window.</summary>
	/// <param name="className">
	/// <para>
	/// A string that specifies the window class name. The class name can be any name registered with RegisterClass or RegisterClassEx,
	/// or any of the predefined control-class names.
	/// </para>
	/// <para>
	/// The maximum length for <c>lpszClassName</c> is 256. If <c>lpszClassName</c> is greater than the maximum length, the
	/// RegisterClassEx function will fail.
	/// </para>
	/// </param>
	/// <param name="hInst">A handle to the instance that contains the window procedure for the class.</param>
	/// <param name="wndProc">
	/// A pointer to the window procedure. You must use the CallWindowProc function to call the window procedure. For more information,
	/// see WindowProc.
	/// </param>
	/// <returns>
	/// A new instance of <see cref="WindowClass"/> with indicated parameters, standard app icon, arrow cursor, and window system color background.
	/// </returns>
	public static WindowClass MakeVisibleWindowClass(string? className, WindowProc? wndProc, HINSTANCE hInst = default) =>
		new(className, hInst.IsNull ? Kernel32.GetModuleHandle() : hInst, wndProc, 0, StdAppIcon, default, StdArrowCursor, WindowBrush);

	/// <summary>Unregisters this window class.</summary>
	/// <returns>
	/// <para>If the function succeeds, the return value is <see langword="true"/>.</para>
	/// <para>
	/// If the class could not be found or if a window still exists that was created with the class, the return value is <see
	/// langword="false"/>. To get extended error information, call GetLastError.
	/// </para>
	/// </returns>
	public bool Unregister() => UnregisterClass(wc.lpszClassName, wc.hInstance);

	/// <summary>An class function that processes messages sent to this class instance.</summary>
	/// <param name="hwnd">A handle to the window.</param>
	/// <param name="msg">The MSG.</param>
	/// <param name="wParam">Additional message information. The contents of this parameter depend on the value of the uMsg parameter.</param>
	/// <param name="lParam">Additional message information. The contents of this parameter depend on the value of the uMsg parameter.</param>
	/// <returns>The return value is the result of the message processing and depends on the message sent.</returns>
	protected virtual IntPtr PrimaryClassWndProc(HWND hwnd, uint msg, IntPtr wParam, IntPtr lParam)
	{
		WindowBase.DebugWriteMessageInfo(msg);
		if (msg == (uint)WindowMessage.WM_NCCREATE)
		{
			try
			{
				var cp = lParam.ToStructure<CREATESTRUCT>().lpCreateParams;
				if (cp != IntPtr.Zero && GCHandle.FromIntPtr(cp).Target is IWindowInit wnd)
					return wnd.InitWndProcOnNCCreate(hwnd, msg, Marshal.GetFunctionPointerForDelegate(wndProc ?? throw new NullReferenceException()), lParam);
			}
			catch { }
		}
		return wndProc?.Invoke(hwnd, msg, wParam, lParam) ?? IntPtr.Zero;
	}

	private static string GetClassName(HWND hwnd)
	{
		StringBuilder sb = new(257);
		Win32Error.ThrowLastErrorIf(User32.GetClassName(hwnd, sb, sb.Capacity), i => i == 0);
		return sb.ToString();
	}
}