// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Runtime.InteropServices;
using CommunityToolkit.Mvvm.Messaging;
using ManagedCommon;
using Microsoft.CmdPal.UI.Helpers;
using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.CmdPal.UI.ViewModels.Dock;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CmdPal.UI.ViewModels.Services;
using Microsoft.CmdPal.UI.ViewModels.Settings;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Composition;
using Microsoft.UI.Composition.SystemBackdrops;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Windows.Foundation;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.Graphics.Dwm;
using Windows.Win32.UI.Accessibility;
using Windows.Win32.UI.Shell;
using Windows.Win32.UI.WindowsAndMessaging;
using WinRT;
using WinRT.Interop;
using WinUIEx;

namespace Microsoft.CmdPal.UI.Dock;

#pragma warning disable SA1402 // File may only contain a single type

public sealed partial class DockWindow : WindowEx,
                                                 IRecipient<BringToTopMessage>,
                                                 IRecipient<RequestShowPaletteAtMessage>,
    IRecipient<QuitMessage>,
    IDisposable
{
#pragma warning disable SA1306 // Field names should begin with lower-case letter
#pragma warning disable SA1310 // Field names should not contain underscore
    private readonly uint WM_TASKBAR_RESTART;
#pragma warning restore SA1310 // Field names should not contain underscore
#pragma warning restore SA1306 // Field names should begin with lower-case letter

    private readonly IThemeService _themeService;
    private readonly ISettingsService _settingsService;
    private readonly DockWindowViewModel _windowViewModel;
    private readonly HiddenOwnerWindowBehavior _hiddenOwnerWindowBehavior = new();

    private HWND _hwnd = HWND.Null;
    private APPBARDATA _appBarData;
    private uint _callbackMessageId;
    private bool _isWindowTopmost;
    private bool _isFullScreenAppOpen;

    private DockSettings _settings;
    private DockViewModel viewModel;
    private DockControl _dock;
    private DesktopAcrylicController? _acrylicController;
    private SystemBackdropConfiguration? _configurationSource;
    private bool _isUpdatingBackdrop;
    private BackdropParameters? _lastAppliedAcrylicBackdrop;
    private DockSize _lastSize;
    private bool _isDisposed;

    // Store the original WndProc
    private WNDPROC? _originalWndProc;
    private WNDPROC? _customWndProc;

    // internal Settings CurrentSettings => _settings;
    public DockWindow()
    {
        var serviceProvider = App.Current.Services;
        var mainSettings = serviceProvider.GetRequiredService<ISettingsService>().Settings;
        _settingsService = serviceProvider.GetRequiredService<ISettingsService>();
        _settingsService.SettingsChanged += SettingsChangedHandler;
        _settings = mainSettings.DockSettings;
        _lastSize = EffectiveDockSize(_settings);

        viewModel = serviceProvider.GetService<DockViewModel>()!;
        _themeService = serviceProvider.GetRequiredService<IThemeService>();
        _themeService.ThemeChanged += ThemeService_ThemeChanged;
        InitializeBackdropSupport();
        _windowViewModel = new DockWindowViewModel(_themeService);
        _dock = new DockControl(viewModel);

        InitializeComponent();
        Root.Children.Add(_dock);
        ExtendsContentIntoTitleBar = true;
        AppWindow.TitleBar.PreferredHeightOption = TitleBarHeightOption.Collapsed;
        _hiddenOwnerWindowBehavior.ShowInTaskbar(this, false);
        if (AppWindow.Presenter is OverlappedPresenter overlappedPresenter)
        {
            overlappedPresenter.SetBorderAndTitleBar(false, false);
            overlappedPresenter.IsResizable = false;
        }

        this.Activated += DockWindow_Activated;

        WeakReferenceMessenger.Default.Register<BringToTopMessage>(this);
        WeakReferenceMessenger.Default.Register<RequestShowPaletteAtMessage>(this);
        WeakReferenceMessenger.Default.Register<QuitMessage>(this);

        _hwnd = GetWindowHandle(this);

        // Subclass the window to intercept messages
        //
        // Set up custom window procedure to listen for display changes
        // LOAD BEARING: If you don't stick the pointer to HotKeyPrc into a
        // member (and instead like, use a local), then the pointer we marshal
        // into the WindowLongPtr will be useless after we leave this function,
        // and our **WindProc will explode**.
        _customWndProc = CustomWndProc;

        _callbackMessageId = PInvoke.RegisterWindowMessage($"CmdPal_ABM_{_hwnd}");

        // TaskbarCreated is the message that's broadcast when explorer.exe
        // restarts. We need to know when that happens to be able to bring our
        // app bar back
        // And this apparently happens on lock screens / hibernates, too
        WM_TASKBAR_RESTART = PInvoke.RegisterWindowMessage("TaskbarCreated");

        var hotKeyPrcPointer = Marshal.GetFunctionPointerForDelegate(_customWndProc);
        _originalWndProc = Marshal.GetDelegateForFunctionPointer<WNDPROC>(PInvoke.SetWindowLongPtr(_hwnd, WINDOW_LONG_PTR_INDEX.GWL_WNDPROC, hotKeyPrcPointer));

        // Disable minimize and maximize box
        var style = (WINDOW_STYLE)PInvoke.GetWindowLong(_hwnd, WINDOW_LONG_PTR_INDEX.GWL_STYLE);
        style &= ~WINDOW_STYLE.WS_MINIMIZEBOX; // Remove WS_MINIMIZEBOX
        style &= ~WINDOW_STYLE.WS_MAXIMIZEBOX; // Remove WS_MAXIMIZEBOX
        _ = PInvoke.SetWindowLong(_hwnd, WINDOW_LONG_PTR_INDEX.GWL_STYLE, (int)style);

        ShowDesktop.AddHook(this);
        var userNotificationFlags = WindowHelper.GetUserNotificationFlags();
        _isFullScreenAppOpen = userNotificationFlags.IsFullscreenState || userNotificationFlags.IsBusy;
        UpdateSettingsOnUiThread();
    }

    private void SettingsChangedHandler(ISettingsService sender, SettingsModel args)
    {
        _settings = args.DockSettings;
        DispatcherQueue.TryEnqueue(UpdateSettingsOnUiThread);
    }

    private void DockWindow_Activated(object sender, WindowActivatedEventArgs args)
    {
        // These are used for removing the very subtle shadow/border that we get from Windows 11
        HwndExtensions.ToggleWindowStyle(_hwnd, false, WindowStyle.TiledWindow);
        unsafe
        {
            BOOL value = false;
            PInvoke.DwmSetWindowAttribute(_hwnd, DWMWINDOWATTRIBUTE.DWMWA_WINDOW_CORNER_PREFERENCE, &value, (uint)sizeof(BOOL));
        }

        UpdateTopmostState();
    }

    private HWND GetWindowHandle(Window window)
    {
        var hwnd = WindowNative.GetWindowHandle(window);
        return new HWND(hwnd);
    }

    private void UpdateSettingsOnUiThread()
    {
        this.viewModel.UpdateSettings(_settings);
        UpdateBackdrop();

        _dock.UpdateSettings(_settings);

        var side = DockSettingsToViews.GetAppBarEdge(_settings.Side);

        if (_appBarData.hWnd != IntPtr.Zero)
        {
            var sameEdge = _appBarData.uEdge == side;
            var sameSize = _lastSize == EffectiveDockSize(_settings);
            if (sameEdge && sameSize)
            {
                UpdateTopmostState();
                return;
            }

            DestroyAppBar(_hwnd);
        }

        CreateAppBar(_hwnd);
        UpdateTopmostState();
    }

    private void InitializeBackdropSupport()
    {
        if (DesktopAcrylicController.IsSupported())
        {
            _configurationSource = new SystemBackdropConfiguration
            {
                IsInputActive = true,
            };
        }
    }

    private void UpdateBackdrop()
    {
        // Prevent re-entrance when backdrop changes trigger theme refresh work.
        if (_isUpdatingBackdrop)
        {
            return;
        }

        _isUpdatingBackdrop = true;

        try
        {
            switch (_settings.Backdrop)
            {
                case DockBackdrop.Transparent:
                    if (SystemBackdrop is not TransparentTintBackdrop)
                    {
                        CleanupBackdropControllers();
                        SetupTransparentBackdrop();
                    }

                    break;

                case DockBackdrop.Acrylic:
                default:
                    SetupDesktopAcrylic(_themeService.CurrentDockTheme.BackdropParameters);
                    break;
            }
        }
        catch (Exception ex)
        {
            Logger.LogError("Failed to update dock backdrop", ex);
        }
        finally
        {
            _isUpdatingBackdrop = false;
        }
    }

    private void SetupTransparentBackdrop()
    {
        if (SystemBackdrop is not TransparentTintBackdrop)
        {
            SystemBackdrop = new TransparentTintBackdrop();
        }

        _lastAppliedAcrylicBackdrop = null;
    }

    private void CleanupBackdropControllers()
    {
        if (_acrylicController is not null)
        {
            _acrylicController.RemoveAllSystemBackdropTargets();
            _acrylicController.Dispose();
            _acrylicController = null;
        }

        _lastAppliedAcrylicBackdrop = null;
    }

    private void SetupDesktopAcrylic(BackdropParameters backdrop)
    {
        var needsAcrylicUpdate = _acrylicController is null || _lastAppliedAcrylicBackdrop != backdrop;
        if (!needsAcrylicUpdate)
        {
            return;
        }

        CleanupBackdropControllers();

        // Fall back to the transparent backdrop if acrylic is not supported.
        if (_configurationSource is null || !DesktopAcrylicController.IsSupported())
        {
            SetupTransparentBackdrop();
            return;
        }

        // DesktopAcrylicController and SystemBackdrop can't be active simultaneously.
        SystemBackdrop = null;

        _acrylicController = new DesktopAcrylicController
        {
            Kind = DesktopAcrylicKind.Thin,
            TintColor = backdrop.TintColor,
            TintOpacity = backdrop.EffectiveOpacity,
            FallbackColor = backdrop.FallbackColor,
            LuminosityOpacity = backdrop.EffectiveLuminosityOpacity,
        };

        // Enable the system backdrop.
        // Note: Be sure to have "using WinRT;" to support the Window.As<...>() call.
        _acrylicController.AddSystemBackdropTarget(this.As<ICompositionSupportsSystemBackdrop>());
        _acrylicController.SetSystemBackdropConfiguration(_configurationSource);
        _lastAppliedAcrylicBackdrop = backdrop;
    }

    private void DisposeAcrylic()
    {
        CleanupBackdropControllers();
        _configurationSource = null;
    }

    private void ThemeService_ThemeChanged(object? sender, ThemeChangedEventArgs e)
    {
        DispatcherQueue.TryEnqueue(() =>
        {
            UpdateBackdrop();

            // ActualTheme / RequestedTheme sync,
            // as pilfered from WindowThemeSynchronizer
            // LOAD BEARING: Changing the RequestedTheme to Dark then Light then target forces
            // a refresh of the theme.
            Root.RequestedTheme = ElementTheme.Dark;
            Root.RequestedTheme = ElementTheme.Light;
            Root.RequestedTheme = _themeService.CurrentDockTheme.Theme;
        });
    }

    private void CreateAppBar(HWND hwnd)
    {
        _appBarData = new APPBARDATA
        {
            cbSize = (uint)Marshal.SizeOf<APPBARDATA>(),
            hWnd = hwnd,
            uCallbackMessage = _callbackMessageId,
        };

        // Register this window as an app bar
        PInvoke.SHAppBarMessage(PInvoke.ABM_NEW, ref _appBarData);

        // Stash the last size we created the bar at, so we know when to hot-
        // reload it
        _lastSize = EffectiveDockSize(_settings);

        UpdateWindowPosition();
    }

    private void DestroyAppBar(HWND hwnd)
    {
        PInvoke.SHAppBarMessage(PInvoke.ABM_REMOVE, ref _appBarData);
        _appBarData = default;
    }

    private void UpdateTopmostState(bool bringToFront = false)
    {
        var shouldStayOnTop = _settings.AlwaysOnTop && !_isFullScreenAppOpen;
        const SET_WINDOW_POS_FLAGS flags = SET_WINDOW_POS_FLAGS.SWP_NOMOVE | SET_WINDOW_POS_FLAGS.SWP_NOSIZE | SET_WINDOW_POS_FLAGS.SWP_NOACTIVATE;

        if (shouldStayOnTop)
        {
            if (_isWindowTopmost && !bringToFront)
            {
                return;
            }

            PInvoke.SetWindowPos(_hwnd, HWND.HWND_TOPMOST, 0, 0, 0, 0, flags);
            _isWindowTopmost = true;
            return;
        }

        if (bringToFront)
        {
            // Win32 trick: briefly set HWND_TOPMOST then immediately clear it
            // with HWND_NOTOPMOST. This brings the window to the foreground
            // without permanently pinning it as topmost.
            PInvoke.SetWindowPos(_hwnd, HWND.HWND_TOPMOST, 0, 0, 0, 0, flags);
        }

        if (!_isWindowTopmost && !bringToFront)
        {
            return;
        }

        var zOrder = _isFullScreenAppOpen ? HWND.HWND_BOTTOM : HWND.HWND_NOTOPMOST;
        PInvoke.SetWindowPos(_hwnd, zOrder, 0, 0, 0, 0, flags);
        _isWindowTopmost = false;
    }

    private void UpdateWindowPosition()
    {
        Logger.LogDebug("UpdateWindowPosition");

        var dpi = PInvoke.GetDpiForWindow(_hwnd);

        var scaleFactor = dpi / 96.0;
        var effectiveSize = EffectiveDockSize(_settings);
        UpdateAppBarDataForEdge(_settings.Side, effectiveSize, scaleFactor);

        // Query and set position
        PInvoke.SHAppBarMessage(PInvoke.ABM_QUERYPOS, ref _appBarData);

        // ABM_QUERYPOS adjusts our rect so we don't overlap other app bars,
        // but it may have shifted our anchored edge without updating the
        // opposite edge. We need to re-apply our desired thickness so the
        // bar keeps its correct size. Without this, a second bar docked to
        // the same side would get a zero-height/width rect and fail to
        // reserve work-area space.
        switch (_settings.Side)
        {
            case DockSide.Top:
                _appBarData.rc.bottom = _appBarData.rc.top + (int)(DockSettingsToViews.HeightForSize(effectiveSize) * scaleFactor);
                break;
            case DockSide.Bottom:
                _appBarData.rc.top = _appBarData.rc.bottom - (int)(DockSettingsToViews.HeightForSize(effectiveSize) * scaleFactor);
                break;
            case DockSide.Left:
                _appBarData.rc.right = _appBarData.rc.left + (int)(DockSettingsToViews.WidthForSize(effectiveSize) * scaleFactor);
                break;
            case DockSide.Right:
                _appBarData.rc.left = _appBarData.rc.right - (int)(DockSettingsToViews.WidthForSize(effectiveSize) * scaleFactor);
                break;
        }

        PInvoke.SHAppBarMessage(PInvoke.ABM_SETPOS, ref _appBarData);

        // TODO: investigate ABS_AUTOHIDE and auto hide bars.
        // I think it's something like this, but I don't totally know
        //   _appBarData.lParam = ABS_ALWAYSONTOP;
        //   _appBarData.lParam = (LPARAM)(int)PInvoke.ABS_AUTOHIDE;
        //   PInvoke.SHAppBarMessage(ABM_SETSTATE, ref _appBarData);
        //   PInvoke.SHAppBarMessage(PInvoke.ABM_SETAUTOHIDEBAR, ref _appBarData);

        // The dock window is borderless (SetBorderAndTitleBar(false, false),
        // IsResizable = false) so no frame compensation is needed — the
        // app bar rect matches the window rect exactly.
        PInvoke.MoveWindow(
            _hwnd,
            _appBarData.rc.left,
            _appBarData.rc.top,
            _appBarData.rc.right - _appBarData.rc.left,
            _appBarData.rc.bottom - _appBarData.rc.top,
            true);
    }

    /// <summary>
    /// Compact mode is only supported for Top/Bottom dock positions.
    /// For Left/Right, always use Default size.
    /// </summary>
    private static DockSize EffectiveDockSize(DockSettings settings)
    {
        var isHorizontal = settings.Side == DockSide.Top || settings.Side == DockSide.Bottom;
        return isHorizontal ? settings.DockSize : DockSize.Default;
    }

    private void UpdateAppBarDataForEdge(DockSide side, DockSize size, double scaleFactor)
    {
        Logger.LogDebug("UpdateAppBarDataForEdge");
        var horizontalHeightDips = DockSettingsToViews.HeightForSize(size);
        var verticalWidthDips = DockSettingsToViews.WidthForSize(size);
        var screenHeight = PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CYSCREEN);
        var screenWidth = PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CXSCREEN);

        if (side == DockSide.Top)
        {
            _appBarData.uEdge = PInvoke.ABE_TOP;
            _appBarData.rc.left = 0;
            _appBarData.rc.top = 0;
            _appBarData.rc.right = screenWidth;
            _appBarData.rc.bottom = (int)(horizontalHeightDips * scaleFactor);
        }
        else if (side == DockSide.Bottom)
        {
            var heightPixels = (int)(horizontalHeightDips * scaleFactor);

            _appBarData.uEdge = PInvoke.ABE_BOTTOM;
            _appBarData.rc.left = 0;
            _appBarData.rc.top = screenHeight - heightPixels;
            _appBarData.rc.right = screenWidth;
            _appBarData.rc.bottom = screenHeight;
        }
        else if (side == DockSide.Left)
        {
            var widthPixels = (int)(verticalWidthDips * scaleFactor);

            _appBarData.uEdge = PInvoke.ABE_LEFT;
            _appBarData.rc.left = 0;
            _appBarData.rc.top = 0;
            _appBarData.rc.right = widthPixels;
            _appBarData.rc.bottom = screenHeight;
        }
        else if (side == DockSide.Right)
        {
            var widthPixels = (int)(verticalWidthDips * scaleFactor);

            _appBarData.uEdge = PInvoke.ABE_RIGHT;
            _appBarData.rc.left = screenWidth - widthPixels;
            _appBarData.rc.top = 0;
            _appBarData.rc.right = screenWidth;
            _appBarData.rc.bottom = screenHeight;
        }
        else
        {
            return;
        }
    }

    private LRESULT CustomWndProc(HWND hwnd, uint msg, WPARAM wParam, LPARAM lParam)
    {
        // check settings changed
        if (msg == PInvoke.WM_SETTINGCHANGE)
        {
            if (wParam == (uint)SYSTEM_PARAMETERS_INFO_ACTION.SPI_SETWORKAREA)
            {
                Logger.LogDebug($"WM_SETTINGCHANGE(SPI_SETWORKAREA)");

                // Use debounced call to throttle rapid successive calls
                DispatcherQueue.TryEnqueue(() => UpdateWindowPosition());
            }
        }
        else if (msg == PInvoke.WM_DISPLAYCHANGE)
        {
            Logger.LogDebug("WM_DISPLAYCHANGE");

            // Use dispatcher to ensure we're on the UI thread
            DispatcherQueue.TryEnqueue(() => UpdateWindowPosition());
        }

        // Intercept WM_SYSCOMMAND to prevent minimize and maximize
        else if (msg == PInvoke.WM_SYSCOMMAND)
        {
            var command = (int)(wParam.Value & 0xFFF0);
            if (command == PInvoke.SC_MINIMIZE || command == PInvoke.SC_MAXIMIZE)
            {
                // Block minimize and maximize commands
                return new LRESULT(0);
            }
        }

        // Stop min/max on WM_WINDOWPOSCHANGING too
        else if (msg == PInvoke.WM_WINDOWPOSCHANGING)
        {
            unsafe
            {
                var pWindowPos = (WINDOWPOS*)lParam.Value;

                // Check if the window is being hidden (minimized) or if flags suggest minimize/maximize
                if ((pWindowPos->flags & SET_WINDOW_POS_FLAGS.SWP_HIDEWINDOW) != 0)
                {
                    // Prevent hiding the window (minimize)
                    pWindowPos->flags &= ~SET_WINDOW_POS_FLAGS.SWP_HIDEWINDOW;
                    pWindowPos->flags |= SET_WINDOW_POS_FLAGS.SWP_SHOWWINDOW;
                }

                // Additional check: if the window position suggests it's being minimized or maximized
                // by checking for dramatic size changes
                if (pWindowPos->cx <= 0 || pWindowPos->cy <= 0)
                {
                    // Prevent zero or negative size changes (minimize)
                    pWindowPos->flags |= SET_WINDOW_POS_FLAGS.SWP_NOSIZE;
                }
            }
        }

        // Handle WM_SIZE to prevent minimize/maximize state changes
        else if (msg == PInvoke.WM_SIZE)
        {
            var sizeType = (int)wParam.Value;
            if (sizeType == PInvoke.SIZE_MINIMIZED || sizeType == PInvoke.SIZE_MAXIMIZED)
            {
                // Block the size change by not calling the original window procedure
                return new LRESULT(0);
            }
        }

        // Handle WM_SHOWWINDOW to prevent hiding (minimize)
        else if (msg == PInvoke.WM_SHOWWINDOW)
        {
            var isBeingShown = wParam.Value != 0;
            if (!isBeingShown)
            {
                // Prevent hiding the window
                return new LRESULT(0);
            }
        }

        // Handle double-click on title bar (non-client area)
        else if (msg == PInvoke.WM_NCLBUTTONDBLCLK)
        {
            var hitTest = (int)wParam.Value;
            if (hitTest == PInvoke.HTCAPTION)
            {
                // Block double-click on title bar to prevent maximize
                return new LRESULT(0);
            }
        }

        // Handle WM_GETMINMAXINFO to allow the dock to be smaller than
        // the default minimum window size (SM_CYMINTRACK ~36px).
        else if (msg == PInvoke.WM_GETMINMAXINFO)
        {
            // Call the original WndProc first so it fills default values,
            // then override the minimum tracking size.
            var result = PInvoke.CallWindowProc(_originalWndProc, hwnd, msg, wParam, lParam);
            unsafe
            {
                var minMaxInfo = (MINMAXINFO*)lParam.Value;
                minMaxInfo->ptMinTrackSize.X = 1;
                minMaxInfo->ptMinTrackSize.Y = 1;
            }

            return result;
        }

        // Handle the AppBarMessage message
        // This is needed to update the position when the work area changes.
        // (notably, when the user toggles auto-hide taskbars)
        else if (msg == _callbackMessageId)
        {
            if (wParam.Value == PInvoke.ABN_POSCHANGED)
            {
                UpdateWindowPosition();
            }
            else if (wParam.Value == PInvoke.ABN_FULLSCREENAPP)
            {
                _isFullScreenAppOpen = lParam != 0;
                UpdateTopmostState();
            }
        }
        else if (msg == WM_TASKBAR_RESTART)
        {
            Logger.LogDebug("WM_TASKBAR_RESTART");

            DispatcherQueue.TryEnqueue(() => CreateAppBar(_hwnd));

            WeakReferenceMessenger.Default.Send<BringToTopMessage>(new(false));
        }

        // Call the original window procedure for all other messages
        return PInvoke.CallWindowProc(_originalWndProc, hwnd, msg, wParam, lParam);
    }

    void IRecipient<BringToTopMessage>.Receive(BringToTopMessage message)
    {
        DispatcherQueue.TryEnqueue(() =>
        {
            UpdateTopmostState(message.BringToFront);
        });
    }

    public void Receive(QuitMessage message)
    {
        DispatcherQueue.TryEnqueue(() =>
        {
            DestroyAppBar(_hwnd);

            this.Close();
        });
    }

    void IRecipient<RequestShowPaletteAtMessage>.Receive(RequestShowPaletteAtMessage message)
    {
        DispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () => RequestShowPaletteOnUiThread(message.PosDips));
    }

    private void RequestShowPaletteOnUiThread(Point posDips)
    {
        // pos is relative to our root. We need to convert to screen coords.
        var rootPosDips = Root.TransformToVisual(null).TransformPoint(new Point(0, 0));
        var screenPosDips = new Point(rootPosDips.X + posDips.X, rootPosDips.Y + posDips.Y);

        var dpi = PInvoke.GetDpiForWindow(_hwnd);
        var scaleFactor = dpi / 96.0;
        var screenPosPixels = new Point(screenPosDips.X * scaleFactor, screenPosDips.Y * scaleFactor);

        var screenWidth = PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CXSCREEN);
        var screenHeight = PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CYSCREEN);

        // Now we're going to find the best position for the palette.

        // We want to anchor the palette on the dock side.
        // on the top:
        //   - anchor to the top, left if we're on the left half of the screen
        //   - anchor to the top, right if we're on the right half of the screen
        // On the left:
        //   - anchor to the top, left if we're on the top half of the screen
        //   - anchor to the bottom, left if we're on the bottom half of the screen
        // On the right:
        //   - anchor to the top, right if we're on the top half of the screen
        //   - anchor to the bottom, right if we're on the bottom half of the screen
        // On the bottom:
        //   - anchor to the bottom, left if we're on the left half of the screen
        //   - anchor to the bottom, right if we're on the right half of the screen
        var onTopHalf = screenPosPixels.Y < screenHeight / 2;
        var onLeftHalf = screenPosPixels.X < screenWidth / 2;
        var onRightHalf = !onLeftHalf;
        var onBottomHalf = !onTopHalf;

        var anchorPoint = _settings.Side switch
        {
            DockSide.Top => onLeftHalf ? AnchorPoint.TopLeft : AnchorPoint.TopRight,
            DockSide.Bottom => onLeftHalf ? AnchorPoint.BottomLeft : AnchorPoint.BottomRight,
            DockSide.Left => onTopHalf ? AnchorPoint.TopLeft : AnchorPoint.BottomLeft,
            DockSide.Right => onTopHalf ? AnchorPoint.TopRight : AnchorPoint.BottomRight,
            _ => AnchorPoint.TopLeft,
        };

        // we also need to slide the anchor point a bit away from the dock
        var paddingDips = 8;
        var paddingPixels = paddingDips * scaleFactor;
        PInvoke.GetWindowRect(_hwnd, out var ourRect);

        // Depending on the side we're on, we need to offset differently
        switch (_settings.Side)
        {
            case DockSide.Top:
                screenPosPixels.Y = ourRect.bottom + paddingPixels;
                break;
            case DockSide.Bottom:
                screenPosPixels.Y = ourRect.top - paddingPixels;
                break;
            case DockSide.Left:
                screenPosPixels.X = ourRect.right + paddingPixels;
                break;
            case DockSide.Right:
                screenPosPixels.X = ourRect.left - paddingPixels;
                break;
        }

        // Now that we know the anchor corner, and where to attempt to place it, we can
        // ask the palette to show itself there.
        WeakReferenceMessenger.Default.Send<ShowPaletteAtMessage>(new(screenPosPixels, anchorPoint));
    }

    public DockWindowViewModel WindowViewModel => _windowViewModel;

    public void Dispose()
    {
        Cleanup();
        GC.SuppressFinalize(this);
    }

    private void DockWindow_Closed(object sender, WindowEventArgs args)
    {
        Dispose();
    }

    private void Cleanup()
    {
        if (_isDisposed)
        {
            return;
        }

        _isDisposed = true;

        _settingsService?.SettingsChanged -= SettingsChangedHandler;

        Activated -= DockWindow_Activated;
        _themeService.ThemeChanged -= ThemeService_ThemeChanged;
        WeakReferenceMessenger.Default.UnregisterAll(this);

        DisposeAcrylic();
        _windowViewModel.Dispose();

        // Remove our app bar registration
        if (_appBarData.hWnd != IntPtr.Zero)
        {
            DestroyAppBar(_hwnd);
        }

        // Unhook the window procedure
        ShowDesktop.RemoveHook();
    }
}

// Thank you to https://stackoverflow.com/a/35422795/1481137
internal static class ShowDesktop
{
    private const string WORKERW = "WorkerW";
    private const string PROGMAN = "Progman";

    private static WINEVENTPROC? _hookProc;
    private static IntPtr _hookHandle = IntPtr.Zero;

    public static void AddHook(Window window)
    {
        if (IsHooked)
        {
            return;
        }

        IsHooked = true;

        _hookProc = (WINEVENTPROC)WinEventCallback;
        _hookHandle = PInvoke.SetWinEventHook(PInvoke.EVENT_SYSTEM_FOREGROUND, PInvoke.EVENT_SYSTEM_FOREGROUND, HMODULE.Null, _hookProc, 0, 0, PInvoke.WINEVENT_OUTOFCONTEXT);
    }

    public static void RemoveHook()
    {
        if (!IsHooked)
        {
            return;
        }

        IsHooked = false;

        PInvoke.UnhookWinEvent((HWINEVENTHOOK)_hookHandle);
        _hookProc = null;
        _hookHandle = IntPtr.Zero;
    }

    private static string GetWindowClass(HWND hwnd)
    {
        unsafe
        {
            fixed (char* c = new char[32])
            {
                _ = PInvoke.GetClassName(hwnd, (PWSTR)c, 32);
                return new string(c);
            }
        }
    }

    internal delegate void WinEventDelegate(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime);

    private static void WinEventCallback(
        HWINEVENTHOOK hWinEventHook,
        uint eventType,
        HWND hwnd,
        int idObject,
        int idChild,
        uint dwEventThread,
        uint dwmsEventTime)
    {
        if (eventType == PInvoke.EVENT_SYSTEM_FOREGROUND)
        {
            var @class = GetWindowClass(hwnd);
            var bringToFront = string.Equals(@class, WORKERW, StringComparison.Ordinal) || string.Equals(@class, PROGMAN, StringComparison.Ordinal);
            if (bringToFront)
            {
                Logger.LogDebug("ShowDesktop invoked. Bring us back");
            }

            WeakReferenceMessenger.Default.Send<BringToTopMessage>(new(bringToFront));
        }
    }

    public static bool IsHooked { get; private set; }
}

internal sealed record BringToTopMessage(bool BringToFront);

internal sealed record RequestShowPaletteAtMessage(Point PosDips);

internal sealed record ShowPaletteAtMessage(Point PosPixels, AnchorPoint Anchor);

internal enum AnchorPoint
{
    TopLeft,
    TopRight,
    BottomLeft,
    BottomRight,
}

#pragma warning restore SA1402 // File may only contain a single type
