#nullable enable
using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using Vanara.Extensions;
using Vanara.InteropServices;
using static Vanara.PInvoke.Kernel32;
using static Vanara.PInvoke.User32;

namespace Vanara.PInvoke;

/// <summary>Interface for a class that holds a window handle.</summary>
/// <seealso cref="Vanara.PInvoke.IHandle"/>
public interface IWindowHandle : IHandle
{
	/// <summary>Gets the window handle.</summary>
	/// <value>The window handle.</value>
	HWND Handle { get; }
}

/// <summary>Simple window wrapper.</summary>
/// <seealso cref="MarshalByRefObject"/>
/// <seealso cref="IDisposable"/>
/// <seealso cref="IHandle"/>
public class WindowBase : MarshalByRefObject, IDisposable, IWindowInstance, IWindowInit, IWindowHandle
{
	/// <summary>A window procedure override delegate.</summary>
	protected WindowProc? customWndProc;

	private static readonly object createWindowSyncObject = new();
	private readonly WeakReference weakThisPtr;
	private bool createdClass = false;
	private SafeHWND? hwnd;
	private bool isDisposed;
	private ParamIndexer? paramIndexer;
	private IntPtr prevWndProcPtr, defWndProc;
	private WindowClass? wCls;
	private WindowProc? wndProc, userDefWndProc;

	/// <summary>Initializes a new instance of the <see cref="WindowBase"/> class without creating the window.</summary>
	public WindowBase() => weakThisPtr = new(this);

	/// <summary>Initializes a new instance of the <see cref="WindowBase"/> class and defines a window procedure to use.</summary>
	/// <param name="wndProcOverride">The window procedure override delegate.</param>
	public WindowBase(WindowProc wndProcOverride) : this() => customWndProc = wndProcOverride;

	internal WindowBase(HWND hwnd) : this() => wCls = WindowClass.GetInstanceFromWindow(hwnd) ?? throw Win32Error.GetLastError().GetException();

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

	/// <summary>Occurs when the window is created and has a valid handle.</summary>
	public event Action? Created;

	/// <summary>Occurs when the window has been destroyed.</summary>
	public event Action? Destroyed;

	/// <summary>Gets the name of the windows class.</summary>
	/// <value>The name of the windows class.</value>
	public string? ClassName => wCls?.ClassName;

	/// <summary>Gets a <see cref="CREATESTRUCT"/> structure with all creation parameters.</summary>
	/// <value>The <see cref="CREATESTRUCT"/> structure with all creation parameters.</value>
	public CREATESTRUCT CreateParam { get; internal set; }

	/// <summary>Gets the window handle.</summary>
	/// <value>The window handle.</value>
	public HWND Handle => hwnd ?? HWND.NULL;

	/// <summary>Gets or sets information about the window.</summary>
	/// <value>The information indexer.</value>
	public ISupportIndexer<WindowLongFlags, IntPtr> Param => paramIndexer ??= new ParamIndexer(this);

	/// <summary>Creates a window and its handle with the specified creation parameters.</summary>
	/// <param name="wc">The window class. If <see langword="null"/>, a new window class is created that is unique to this window.</param>
	/// <param name="text">
	/// The window name. If the window style specifies a title bar, the window title pointed to by lpWindowName is displayed in the title
	/// bar. When using CreateWindow to create controls, such as buttons, check boxes, and static controls, use lpWindowName to specify the
	/// text of the control. When creating a static control with the <c>SS_ICON</c> style, use lpWindowName to specify the icon name or
	/// identifier. To specify an identifier, use the syntax "#num".
	/// </param>
	/// <param name="size">
	/// The width and height, in device units, of the window. For overlapped windows, nWidth is the window's width, in screen coordinates, or
	/// <c>CW_USEDEFAULT</c>. If nWidth is <c>CW_USEDEFAULT</c>, the system selects a default width and height for the window; the default
	/// width extends from the initial x-coordinates to the right edge of the screen; the default height extends from the initial
	/// y-coordinate to the top of the icon area. <c>CW_USEDEFAULT</c> is valid only for overlapped windows; if <c>CW_USEDEFAULT</c> is
	/// specified for a pop-up or child window, the nWidth and nHeight parameter are set to zero.
	/// </param>
	/// <param name="position">
	/// <para>
	/// The initial vertical position of the window. For an overlapped or pop-up window, the x parameter is the initial x-coordinate of the
	/// window's upper-left corner and the y parameter is the initial y-coordinate of the window's upper-left corner, in screen coordinates.
	/// For a child window, x is the x-coordinate of the upper-left corner of the window relative to the upper-left corner of the parent
	/// window's client area and y is the initial y-coordinate of the upper-left corner of the child window relative to the upper-left
	/// corner. If x is set to <c>CW_USEDEFAULT</c>, the system selects the default position for the window's upper-left corner and ignores
	/// the y parameter. <c>CW_USEDEFAULT</c> is valid only for overlapped windows; if it is specified for a pop-up or child window, the x
	/// and y parameters are set to zero.
	/// </para>
	/// <para>
	/// If an overlapped window is created with the <c>WS_VISIBLE</c> style bit set and the x parameter is set to <c>CW_USEDEFAULT</c>, then
	/// the y parameter determines how the window is shown. If the y parameter is <c>CW_USEDEFAULT</c>, then the window manager calls
	/// ShowWindow with the <c>SW_SHOW</c> flag after the window has been created. If the y parameter is some other value, then the window
	/// manager calls <c>ShowWindow</c> with that value as the nCmdShow parameter.
	/// </para>
	/// </param>
	/// <param name="style">
	/// <para>Type: <c>DWORD</c></para>
	/// <para>
	/// The style of the window being created. This parameter can be a combination of the window style values, plus the control styles
	/// indicated in the Remarks section.
	/// </para>
	/// </param>
	/// <param name="exStyle">The extended window style of the window being created. For a list of possible values,see Extended Window Styles.</param>
	/// <param name="parent">
	/// <para>
	/// A handle to the parent or owner window of the window being created. To create a child window or an owned window, supply a valid
	/// window handle. This parameter is optional for pop-up windows.
	/// </para>
	/// <para>To create a message-only window, supply <c>HWND_MESSAGE</c> or a handle to an existing message-only window.</para>
	/// </param>
	/// <param name="hMenu">
	/// <para>Type: <c>HMENU</c></para>
	/// <para>
	/// A handle to a menu, or specifies a child-window identifier, depending on the window style. For an overlapped or pop-up window, hMenu
	/// identifies the menu to be used with the window; it can be <c>NULL</c> if the class menu is to be used. For a child window, hMenu
	/// specifies the child-window identifier, an integer value used by a dialog box control to notify its parent about events. The
	/// application determines the child-window identifier; it must be unique for all child windows with the same parent window.
	/// </para>
	/// </param>
	public virtual void CreateHandle(WindowClass? wc = null, string? text = null, SIZE? size = default, POINT? position = default,
		WindowStyles style = 0, WindowStylesEx exStyle = 0, HWND parent = default, HMENU hMenu = default)
	{
		lock (this)
		{
			CheckDetached();
			lock (createWindowSyncObject)
			{
				if (hwnd is not null)
					return;

				if ((wCls = wc) is null)
				{
					wCls = WindowClass.MakeVisibleWindowClass($"{GetType().Name}+{Guid.NewGuid()}", null);
					createdClass = true;
				}
				size ??= new(CW_USEDEFAULT, CW_USEDEFAULT);
				position ??= new(CW_USEDEFAULT, CW_USEDEFAULT);
				GCHandle gcWnd = GCHandle.Alloc(this);
				try
				{
					if (text?.Length > short.MaxValue)
						text = text.Substring(0, short.MaxValue);
					hwnd = Win32Error.ThrowLastErrorIfInvalid(CreateWindowEx(exStyle, wCls.ClassName, text, style, position.Value.X,
						position.Value.Y, size.Value.Width, size.Value.Height, parent, hMenu, wCls.wc.hInstance, GCHandle.ToIntPtr(gcWnd)));
				}
				finally
				{
					gcWnd.Free();
				}
			}
		}
	}

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

	/// <inhertdoc/>
	IntPtr IHandle.DangerousGetHandle() => hwnd?.DangerousGetHandle() ?? IntPtr.Zero;

	/// <inheritdoc/>
	IntPtr IWindowInit.InitWndProcOnNCCreate(HWND hwnd, uint msg, IntPtr wParam, IntPtr lParam)
	{
		lock (this)
			Attach(hwnd, true);
		return InternalWndProc(hwnd, msg, IntPtr.Zero, lParam);
	}

	/// <summary>Writes message information to the debugger.</summary>
	/// <param name="msg">The message code.</param>
	/// <param name="sourceFilePath">The source file path.</param>
	/// <param name="caller">The calling method.</param>
	[Conditional("DEBUG")]
	internal static void DebugWriteMessageInfo(uint msg, [CallerFilePath] string sourceFilePath = "", [CallerMemberName] string? caller = "")
	{
		if (Enum.IsDefined(typeof(WindowMessage), (WindowMessage)msg))
			Debug.WriteLine($"{caller}={(WindowMessage)msg} ({sourceFilePath})");
		else
			Debug.WriteLine($"{caller}={msg:x} ({sourceFilePath})");
	}

	/// <summary>Writes method information to the debugger.</summary>
	/// <param name="sourceFilePath">The source file path.</param>
	/// <param name="caller">The calling method.</param>
	[Conditional("DEBUG")]
	internal static void DebugWriteMethod([CallerFilePath] string sourceFilePath = "", [CallerMemberName] string? caller = "") =>
			Debug.WriteLine($"{caller} ({sourceFilePath})");

	/// <summary>
	/// Attaches the specified window handle to this instance and associates this instance's <see cref="WindowProc"/> handlers using GWL_WNDPROC.
	/// </summary>
	/// <param name="hwnd">The window handle.</param>
	/// <param name="own">if set to <see langword="true"/>, the window will be destroyed and handle released on disposal.</param>
	protected virtual void Attach(HWND hwnd, bool own)
	{
		CheckDisposed();
		Debug.Assert(!hwnd.IsNull, "Attempt assigning invalid handle.");
		CheckDetached();

		// Set handle
		this.hwnd = new((IntPtr)hwnd, own);

		// Set handle's WNDPROC to this class'
		userDefWndProc ??= DefWindowProc;
		defWndProc = Param[WindowLongFlags.GWLP_WNDPROC];
		wndProc = InternalWndProc;
		Param[WindowLongFlags.GWLP_WNDPROC] = Marshal.GetFunctionPointerForDelegate(wndProc);
		Debug.Assert(defWndProc != Param[WindowLongFlags.GWLP_WNDPROC], "Subclassed ourself!");

		OnHandleChanged(hwnd);
	}

	/// <summary>Invokes the default window procedure associated with this window.</summary>
	/// <param name="hwnd">A handle to the window procedure that received the message.</param>
	/// <param name="msg">The message.</param>
	/// <param name="wParam">
	/// Additional message information. The content of this parameter depends on the value of the <paramref name="msg"/> parameter.
	/// </param>
	/// <param name="lParam">
	/// Additional message information. The content of this parameter depends on the value of the <paramref name="msg"/> parameter.
	/// </param>
	/// <returns>The return value is the result of the message processing and depends on the message.</returns>
	protected virtual IntPtr DefWndProc(HWND hwnd, uint msg, IntPtr wParam, IntPtr lParam) =>
		defWndProc == default ? DefWindowProc(hwnd, msg, wParam, lParam) : CallWindowProc(defWndProc, hwnd, msg, wParam, lParam);

	/// <summary>
	/// Detaches this instance's <see cref="WindowProc"/> handlers using GWL_WNDPROC, restoring previous assignment, and destroys the window
	/// and closes the handle if owned.
	/// </summary>
	protected virtual void Detach()
	{
		if (hwnd is null) return;

		if (!hwnd.IsInvalid && prevWndProcPtr != IntPtr.Zero)
		{
			Param[WindowLongFlags.GWLP_WNDPROC] = prevWndProcPtr;
			prevWndProcPtr = IntPtr.Zero;
		}

		hwnd.Dispose();
		hwnd = null;

		OnHandleChanged(HWND.NULL);
	}

	/// <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 (isDisposed)
			return;
		lock (this)
			Detach();
		if (createdClass) wCls?.Unregister();
		isDisposed = true;
	}

	/// <summary>Called when this classes <see cref="InternalWndProc"/> method becomes primary.</summary>
	protected virtual void OnHandleChanged(HWND newHandle) => DebugWriteMethod();

	/// <summary>Called when the <see cref="WindowProc"/> call throws an exception.</summary>
	/// <param name="ex">The exception.</param>
	/// <returns>
	/// <see langword="true"/> if the exception is handled and does not need to be thrown; <see langword="false"/> to throw the exception
	/// from within the <see cref="WindowProc"/> procedure.
	/// </returns>
	protected virtual bool OnWndProcException(Exception ex) => false;

	/// <summary>Invokes the default window procedure associated with this window.</summary>
	/// <param name="hwnd">A handle to the window procedure that received the message.</param>
	/// <param name="msg">The message.</param>
	/// <param name="wParam">
	/// Additional message information. The content of this parameter depends on the value of the <paramref name="msg"/> parameter.
	/// </param>
	/// <param name="lParam">
	/// Additional message information. The content of this parameter depends on the value of the <paramref name="msg"/> parameter.
	/// </param>
	/// <returns>The return value is the result of the message processing and depends on the message.</returns>
	protected virtual IntPtr WndProc(HWND hwnd, uint msg, IntPtr wParam, IntPtr lParam) => DefWndProc(hwnd, msg, wParam, lParam);

	[DebuggerStepThrough]
	private void CheckDetached()
	{
		if (hwnd is not null)
			throw new InvalidOperationException("Attempt to overwrite existing window handle.");
	}

	[DebuggerStepThrough]
	private void CheckDisposed()
	{
		if (isDisposed) throw new ObjectDisposedException(nameof(WindowBase));
	}

	/// <summary>
	/// This is the real <see cref="WindowProc"/> instance for the window. It calls either <see cref="WndProc(HWND, uint, IntPtr, IntPtr)"/>
	/// or <see cref="DefWndProc(HWND, uint, IntPtr, IntPtr)"/> depending on status.
	/// <para>It is responsible for setting <see cref="CreateParam"/> and calling <see cref="Created"/> and <see cref="Destroyed"/>.</para>
	/// </summary>
	/// <param name="hwnd">A handle to the window procedure that received the message.</param>
	/// <param name="msg">The message.</param>
	/// <param name="wParam">
	/// Additional message information. The content of this parameter depends on the value of the <paramref name="msg"/> parameter.
	/// </param>
	/// <param name="lParam">
	/// Additional message information. The content of this parameter depends on the value of the <paramref name="msg"/> parameter.
	/// </param>
	/// <returns>The return value is the result of the message processing and depends on the message.</returns>
	private IntPtr InternalWndProc(HWND hwnd, uint msg, IntPtr wParam, IntPtr lParam)
	{
		DebugWriteMessageInfo(msg);
		if (msg == (uint)WindowMessage.WM_NCCREATE)
		{
			CreateParam = lParam.ToStructure<CREATESTRUCT>();
			return (IntPtr)1;
		}

		try
		{
			if (weakThisPtr.IsAlive && weakThisPtr.Target != null)
				return (customWndProc ?? WndProc)(hwnd, msg, wParam, lParam);
			else
				return DefWndProc(hwnd, msg, wParam, lParam);
		}
		catch (Exception ex)
		{
			if (!OnWndProcException(ex))
				throw;
			return IntPtr.Zero;
		}
		finally
		{
			switch ((WindowMessage)msg)
			{
				case WindowMessage.WM_CREATE:
					Created?.Invoke();
					break;
				case WindowMessage.WM_NCDESTROY:
					PostQuitMessage(0);
					Destroyed?.Invoke();
					break;
			}
		}
	}

	private class ParamIndexer : ISupportIndexer<WindowLongFlags, IntPtr>
	{
		private readonly IWindowInstance win;

		public ParamIndexer(IWindowInstance win) => this.win = win;

		public IntPtr this[WindowLongFlags flag]
		{
			get => win.Handle != HWND.NULL ? GetWindowLongAuto(win.Handle, flag) : throw new ObjectDisposedException(nameof(IWindowInstance));
			set { if (win.Handle != HWND.NULL) SetWindowLong(win.Handle, flag, value); else throw new ObjectDisposedException(nameof(IWindowInstance)); }
		}
	}
}