/********************************************************
*                                                       *
*   Copyright (C) Microsoft. All rights reserved.       *
*                                                       *
********************************************************/
#include "pch.h"
#include "NonClientIslandWindow.h"

extern "C" IMAGE_DOS_HEADER __ImageBase;

using namespace winrt::Windows::UI;
using namespace winrt::Windows::UI::Composition;
using namespace winrt::Windows::UI::Xaml;
using namespace winrt::Windows::UI::Xaml::Hosting;
using namespace winrt::Windows::Foundation::Numerics;
using namespace ::Microsoft::Console::Types;

constexpr int RECT_WIDTH(const RECT* const pRect)
{
    return pRect->right - pRect->left;
}
constexpr int RECT_HEIGHT(const RECT* const pRect)
{
    return pRect->bottom - pRect->top;
}

NonClientIslandWindow::NonClientIslandWindow() noexcept :
    IslandWindow{},
    _isMaximized{ false }
{
}

NonClientIslandWindow::~NonClientIslandWindow()
{
}

// Method Description:
// - Called when the app's size changes. When that happens, the size of the drag
//   bar may have changed. If it has, we'll need to update the WindowRgn of the
//   interop window.
// Arguments:
// - <unused>
// Return Value:
// - <none>
void NonClientIslandWindow::OnDragBarSizeChanged(winrt::Windows::Foundation::IInspectable /*sender*/,
                                                 winrt::Windows::UI::Xaml::SizeChangedEventArgs /*eventArgs*/)
{
    _UpdateDragRegion();
}

void NonClientIslandWindow::OnAppInitialized()
{
    IslandWindow::OnAppInitialized();
}

void NonClientIslandWindow::Initialize()
{
    IslandWindow::Initialize();

    // Set up our grid of content. We'll use _rootGrid as our root element.
    // There will be two children of this grid - the TitlebarControl, and the
    // "client content"
    _rootGrid.Children().Clear();
    Controls::RowDefinition titlebarRow{};
    Controls::RowDefinition contentRow{};
    titlebarRow.Height(GridLengthHelper::Auto());

    _rootGrid.RowDefinitions().Append(titlebarRow);
    _rootGrid.RowDefinitions().Append(contentRow);

    // Create our titlebar control
    _titlebar = winrt::TerminalApp::TitlebarControl{ reinterpret_cast<uint64_t>(GetHandle()) };
    _dragBar = _titlebar.DragBar();

    _rootGrid.SizeChanged({ this, &NonClientIslandWindow::OnDragBarSizeChanged });

    _rootGrid.Children().Append(_titlebar);

    Controls::Grid::SetRow(_titlebar, 0);
}

// Method Description:
// - Set the content of the "client area" of our window to the given content.
// Arguments:
// - content: the new UI element to use as the client content
// Return Value:
// - <none>
void NonClientIslandWindow::SetContent(winrt::Windows::UI::Xaml::UIElement content)
{
    _clientContent = content;

    _rootGrid.Children().Append(content);

    // SetRow only works on FrameworkElement's, so cast it to a FWE before
    // calling. We know that our content is a Grid, so we don't need to worry
    // about this.
    const auto fwe = content.try_as<winrt::Windows::UI::Xaml::FrameworkElement>();
    if (fwe)
    {
        Controls::Grid::SetRow(fwe, 1);
    }
}

// Method Description:
// - Set the content of the "titlebar area" of our window to the given content.
// Arguments:
// - content: the new UI element to use as the titlebar content
// Return Value:
// - <none>
void NonClientIslandWindow::SetTitlebarContent(winrt::Windows::UI::Xaml::UIElement content)
{
    _titlebar.Content(content);

    // When the size of the titlebar content changes, we want to make sure to
    // update the size of the drag region as well.
    const auto fwe = content.try_as<winrt::Windows::UI::Xaml::FrameworkElement>();
    if (fwe)
    {
        fwe.SizeChanged({ this, &NonClientIslandWindow::OnDragBarSizeChanged });
    }
}

RECT NonClientIslandWindow::GetDragAreaRect() const noexcept
{
    if (_dragBar)
    {
        const auto scale = GetCurrentDpiScale();
        const auto transform = _dragBar.TransformToVisual(_rootGrid);
        const auto logicalDragBarRect = winrt::Windows::Foundation::Rect{
            0.0f,
            0.0f,
            static_cast<float>(_dragBar.ActualWidth()),
            static_cast<float>(_dragBar.ActualHeight())
        };
        const auto clientDragBarRect = transform.TransformBounds(logicalDragBarRect);
        RECT dragBarRect = {
            static_cast<LONG>(clientDragBarRect.X * scale),
            static_cast<LONG>(clientDragBarRect.Y * scale),
            static_cast<LONG>((clientDragBarRect.Width + clientDragBarRect.X) * scale),
            static_cast<LONG>((clientDragBarRect.Height + clientDragBarRect.Y) * scale),
        };
        return dragBarRect;
    }

    return RECT{};
}

// Method Description:
// - called when the size of the window changes for any reason. Updates the
//   sizes of our child Xaml Islands to match our new sizing.
void NonClientIslandWindow::OnSize(const UINT width, const UINT height)
{
    if (!_interopWindowHandle)
    {
        return;
    }

    const auto scale = GetCurrentDpiScale();
    const auto dpi = ::GetDpiForWindow(_window.get());

    const auto dragY = ::GetSystemMetricsForDpi(SM_CYDRAG, dpi);
    const auto dragX = ::GetSystemMetricsForDpi(SM_CXDRAG, dpi);

    // If we're maximized, we don't want to use the frame as our margins,
    // instead we want to use the margins from the maximization. If we included
    // the left&right sides of the frame in this calculation while maximized,
    // you' have a few pixels of the window border on the sides while maximized,
    // which most apps do not have.
    const auto bordersWidth = _isMaximized ?
                                  (_maximizedMargins.cxLeftWidth + _maximizedMargins.cxRightWidth) :
                                  (dragX * 2);
    const auto bordersHeight = _isMaximized ?
                                   (_maximizedMargins.cyBottomHeight + _maximizedMargins.cyTopHeight) :
                                   (dragY * 2);

    const auto windowsWidth = width - bordersWidth;
    const auto windowsHeight = height - bordersHeight;
    const auto xPos = _isMaximized ? _maximizedMargins.cxLeftWidth : dragX;
    const auto yPos = _isMaximized ? _maximizedMargins.cyTopHeight : dragY;

    if (_rootGrid)
    {
        winrt::Windows::Foundation::Size size{ (windowsWidth / scale) + 0.5f, (windowsHeight / scale) + 0.5f };
        _rootGrid.Height(size.Height);
        _rootGrid.Width(size.Width);
        _rootGrid.Measure(size);
        winrt::Windows::Foundation::Rect finalRect{};
        _rootGrid.Arrange(finalRect);
    }

    // I'm not sure that HWND_BOTTOM does anything differnet than HWND_TOP for us.
    winrt::check_bool(SetWindowPos(_interopWindowHandle,
                                   HWND_BOTTOM,
                                   xPos,
                                   yPos,
                                   windowsWidth,
                                   windowsHeight,
                                   SWP_SHOWWINDOW));
}

// Method Description:
// - Update the region of our window that is the draggable area. This happens in
//   response to a OnDragBarSizeChanged event. We'll calculate the areas of the
//   window that we want to display XAML content in, and set the window region
//   of our child xaml-island window to that region. That way, the parent window
//   will still get NCHITTEST'ed _outside_ the XAML content area, for things
//   like dragging and resizing.
// Arguments:
// - <none>
// Return Value:
// - <none>
void NonClientIslandWindow::_UpdateDragRegion()
{
    if (_dragBar)
    {
        // TODO:GH#1897 This is largely duplicated from OnSize, and we should do
        // better than that.
        const auto windowRect = GetWindowRect();
        const auto width = windowRect.right - windowRect.left;
        const auto height = windowRect.bottom - windowRect.top;

        const auto scale = GetCurrentDpiScale();
        const auto dpi = ::GetDpiForWindow(_window.get());

        const auto dragY = ::GetSystemMetricsForDpi(SM_CYDRAG, dpi);
        const auto dragX = ::GetSystemMetricsForDpi(SM_CXDRAG, dpi);

        // If we're maximized, we don't want to use the frame as our margins,
        // instead we want to use the margins from the maximization. If we included
        // the left&right sides of the frame in this calculation while maximized,
        // you' have a few pixels of the window border on the sides while maximized,
        // which most apps do not have.
        const auto bordersWidth = _isMaximized ?
                                      (_maximizedMargins.cxLeftWidth + _maximizedMargins.cxRightWidth) :
                                      (dragX * 2);
        const auto bordersHeight = _isMaximized ?
                                       (_maximizedMargins.cyBottomHeight + _maximizedMargins.cyTopHeight) :
                                       (dragY * 2);

        const auto windowsWidth = width - bordersWidth;
        const auto windowsHeight = height - bordersHeight;
        const auto xPos = _isMaximized ? _maximizedMargins.cxLeftWidth : dragX;
        const auto yPos = _isMaximized ? _maximizedMargins.cyTopHeight : dragY;

        const auto dragBarRect = GetDragAreaRect();
        const auto nonClientHeight = dragBarRect.bottom - dragBarRect.top;

        auto nonClientRegion = wil::unique_hrgn(CreateRectRgn(0, 0, 0, 0));
        auto nonClientLeftRegion = wil::unique_hrgn(CreateRectRgn(0, 0, dragBarRect.left, nonClientHeight));
        auto nonClientRightRegion = wil::unique_hrgn(CreateRectRgn(dragBarRect.right, 0, windowsWidth, nonClientHeight));
        winrt::check_bool(CombineRgn(nonClientRegion.get(), nonClientLeftRegion.get(), nonClientRightRegion.get(), RGN_OR));

        _dragBarRegion = wil::unique_hrgn(CreateRectRgn(0, 0, 0, 0));
        auto clientRegion = wil::unique_hrgn(CreateRectRgn(0, nonClientHeight, windowsWidth, windowsHeight));
        winrt::check_bool(CombineRgn(_dragBarRegion.get(), nonClientRegion.get(), clientRegion.get(), RGN_OR));
        winrt::check_bool(SetWindowRgn(_interopWindowHandle, _dragBarRegion.get(), true));
    }
}

// Method Description:
// Hit test the frame for resizing and moving.
// Method Description:
// - Hit test the frame for resizing and moving.
// Arguments:
// - ptMouse: the mouse point being tested, in absolute (NOT WINDOW) coordinates.
// Return Value:
// - one of the values from
//  https://docs.microsoft.com/en-us/windows/desktop/inputdev/wm-nchittest#return-value
//   corresponding to the area of the window that was hit
// NOTE:
// - Largely taken from code on:
// https://docs.microsoft.com/en-us/windows/desktop/dwm/customframe
[[nodiscard]] LRESULT NonClientIslandWindow::HitTestNCA(POINT ptMouse) const noexcept
{
    // Get the window rectangle.
    RECT rcWindow = BaseWindow::GetWindowRect();

    MARGINS margins = GetFrameMargins();

    // Get the frame rectangle, adjusted for the style without a caption.
    RECT rcFrame = { 0 };
    auto expectedStyle = WS_OVERLAPPEDWINDOW;
    WI_ClearAllFlags(expectedStyle, WS_CAPTION);
    AdjustWindowRectEx(&rcFrame, expectedStyle, false, 0);

    // Determine if the hit test is for resizing. Default middle (1,1).
    unsigned short uRow = 1;
    unsigned short uCol = 1;
    bool fOnResizeBorder = false;

    // Determine if the point is at the top or bottom of the window.
    if (ptMouse.y >= rcWindow.top && ptMouse.y < rcWindow.top + margins.cyTopHeight)
    {
        fOnResizeBorder = (ptMouse.y < (rcWindow.top - rcFrame.top));
        uRow = 0;
    }
    else if (ptMouse.y < rcWindow.bottom && ptMouse.y >= rcWindow.bottom - margins.cyBottomHeight)
    {
        uRow = 2;
    }

    // Determine if the point is at the left or right of the window.
    if (ptMouse.x >= rcWindow.left && ptMouse.x < rcWindow.left + margins.cxLeftWidth)
    {
        uCol = 0; // left side
    }
    else if (ptMouse.x < rcWindow.right && ptMouse.x >= rcWindow.right - margins.cxRightWidth)
    {
        uCol = 2; // right side
    }

    // clang-format off
    // Hit test (HTTOPLEFT, ... HTBOTTOMRIGHT)
    const auto topHt = fOnResizeBorder ? HTTOP : HTCAPTION;
    LRESULT hitTests[3][3] = {
        { HTTOPLEFT,    topHt,      HTTOPRIGHT },
        { HTLEFT,       HTNOWHERE,  HTRIGHT },
        { HTBOTTOMLEFT, HTBOTTOM,   HTBOTTOMRIGHT },
    };
    // clang-format on

    return hitTests[uRow][uCol];
}

// Method Description:
// - Get the size of the borders we want to use. The sides and bottom will just
//   be big enough for resizing, but the top will be as big as we need for the
//   non-client content.
// Return Value:
// - A MARGINS struct containing the border dimensions we want.
MARGINS NonClientIslandWindow::GetFrameMargins() const noexcept
{
    const auto scale = GetCurrentDpiScale();
    const auto dpi = ::GetDpiForWindow(_window.get());
    const auto windowMarginSides = ::GetSystemMetricsForDpi(SM_CXDRAG, dpi);
    const auto windowMarginBottom = ::GetSystemMetricsForDpi(SM_CXDRAG, dpi);

    const auto dragBarRect = GetDragAreaRect();
    const auto nonClientHeight = dragBarRect.bottom - dragBarRect.top;

    MARGINS margins{ 0 };
    margins.cxLeftWidth = windowMarginSides;
    margins.cxRightWidth = windowMarginSides;
    margins.cyBottomHeight = windowMarginBottom;
    margins.cyTopHeight = nonClientHeight + windowMarginBottom;

    return margins;
}

// Method Description:
// - Updates the borders of our window frame, using DwmExtendFrameIntoClientArea.
// Arguments:
// - <none>
// Return Value:
// - the HRESULT returned by DwmExtendFrameIntoClientArea.
[[nodiscard]] HRESULT NonClientIslandWindow::_UpdateFrameMargins() const noexcept
{
    // Set frame margins with just a single pixel on the bottom. We don't
    // really want a window frame at all - we're drawing all of it. We
    // especially don't want a top margin - that's where the caption buttons
    // are, and we're drawing those. So just set a single pixel on the bottom,
    // because the method won't work with {0}.
    MARGINS margins = { 0, 0, 0, 1 };

    // Extend the frame into the client area.
    return DwmExtendFrameIntoClientArea(_window.get(), &margins);
}

// Routine Description:
// - Gets the maximum possible window rectangle in pixels. Based on the monitor
//   the window is on or the primary monitor if no window exists yet.
// Arguments:
// - prcSuggested - If we were given a suggested rectangle for where the window
//                  is going, we can pass it in here to find out the max size
//                  on that monitor.
//                - If this value is zero and we had a valid window handle,
//                  we'll use that instead. Otherwise the value of 0 will make
//                  us use the primary monitor.
// - pDpiSuggested - The dpi that matches the suggested rect. We will attempt to
//                   compute this during the function, but if we fail for some
//                   reason, the original value passed in will be left untouched.
// Return Value:
// - RECT containing the left, right, top, and bottom positions from the desktop
//   origin in pixels. Measures the outer edges of the potential window.
// NOTE:
// Heavily taken from WindowMetrics::GetMaxWindowRectInPixels in conhost.
RECT NonClientIslandWindow::GetMaxWindowRectInPixels(const RECT* const prcSuggested, _Out_opt_ UINT* pDpiSuggested)
{
    // prepare rectangle
    RECT rc = *prcSuggested;

    // use zero rect to compare.
    RECT rcZero;
    SetRectEmpty(&rcZero);

    // First get the monitor pointer from either the active window or the default location (0,0,0,0)
    HMONITOR hMonitor = nullptr;

    // NOTE: We must use the nearest monitor because sometimes the system moves
    // the window around into strange spots while performing snap and Win+D
    // operations. Those operations won't work correctly if we use
    // MONITOR_DEFAULTTOPRIMARY.
    if (!EqualRect(&rc, &rcZero))
    {
        // For invalid window handles or when we were passed a non-zero
        // suggestion rectangle, get the monitor from the rect.
        hMonitor = MonitorFromRect(&rc, MONITOR_DEFAULTTONEAREST);
    }
    else
    {
        // Otherwise, get the monitor from the window handle.
        hMonitor = MonitorFromWindow(_window.get(), MONITOR_DEFAULTTONEAREST);
    }

    // If for whatever reason there is no monitor, we're going to give back
    // whatever we got since we can't figure anything out. We won't adjust the
    // DPI either. That's OK. DPI doesn't make much sense with no display.
    if (nullptr == hMonitor)
    {
        return rc;
    }

    // Now obtain the monitor pixel dimensions
    MONITORINFO MonitorInfo = { 0 };
    MonitorInfo.cbSize = sizeof(MONITORINFO);

    GetMonitorInfoW(hMonitor, &MonitorInfo);

    // We have to make a correction to the work area. If we actually consume the
    // entire work area (by maximizing the window). The window manager will
    // render the borders off-screen. We need to pad the work rectangle with the
    // border dimensions to represent the actual max outer edges of the window
    // rect.
    WINDOWINFO wi = { 0 };
    wi.cbSize = sizeof(WINDOWINFO);
    GetWindowInfo(_window.get(), &wi);

    // In non-full screen, we want to only use the work area (avoiding the task bar space)
    rc = MonitorInfo.rcWork;

    if (pDpiSuggested != nullptr)
    {
        UINT monitorDpiX;
        UINT monitorDpiY;
        if (SUCCEEDED(GetDpiForMonitor(hMonitor, MDT_EFFECTIVE_DPI, &monitorDpiX, &monitorDpiY)))
        {
            *pDpiSuggested = monitorDpiX;
        }
        else
        {
            *pDpiSuggested = GetDpiForWindow(_window.get());
        }
    }

    return rc;
}

// Method Description:
// - Handle window messages from the message loop.
// Arguments:
// - message: A window message ID identifying the message.
// - wParam: The contents of this parameter depend on the value of the message parameter.
// - lParam: The contents of this parameter depend on the value of the message parameter.
// Return Value:
// - The return value is the result of the message processing and depends on the
//   message sent.
[[nodiscard]] LRESULT NonClientIslandWindow::MessageHandler(UINT const message,
                                                            WPARAM const wParam,
                                                            LPARAM const lParam) noexcept
{
    LRESULT lRet = 0;

    // First call DwmDefWindowProc. This might handle things like the
    // min/max/close buttons for us.
    const bool dwmHandledMessage = DwmDefWindowProc(_window.get(), message, wParam, lParam, &lRet);

    switch (message)
    {
    case WM_ACTIVATE:
    {
        _HandleActivateWindow();
        break;
    }
    case WM_NCCALCSIZE:
    {
        if (wParam == false)
        {
            return 0;
        }
        // Handle the non-client size message.
        if (wParam == TRUE && lParam)
        {
            // Calculate new NCCALCSIZE_PARAMS based on custom NCA inset.
            NCCALCSIZE_PARAMS* pncsp = reinterpret_cast<NCCALCSIZE_PARAMS*>(lParam);

            pncsp->rgrc[0].left = pncsp->rgrc[0].left + 0;
            pncsp->rgrc[0].top = pncsp->rgrc[0].top + 0;
            pncsp->rgrc[0].right = pncsp->rgrc[0].right - 0;
            pncsp->rgrc[0].bottom = pncsp->rgrc[0].bottom - 0;

            return 0;
        }
        break;
    }
    case WM_NCHITTEST:
    {
        if (dwmHandledMessage)
        {
            return lRet;
        }

        // Handle hit testing in the NCA if not handled by DwmDefWindowProc.
        if (lRet == 0)
        {
            lRet = HitTestNCA({ GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) });
            if (lRet != HTNOWHERE)
            {
                return lRet;
            }
        }
        break;
    }

    case WM_EXITSIZEMOVE:
    {
        ForceResize();
        break;
    }

    case WM_PAINT:
    {
        if (!_dragBar)
        {
            return 0;
        }

        PAINTSTRUCT ps{ 0 };
        const auto hdc = wil::BeginPaint(_window.get(), &ps);
        if (hdc.get())
        {
            const auto scale = GetCurrentDpiScale();
            const auto dpi = ::GetDpiForWindow(_window.get());
            // Get the dimensions of the drag borders for the sides of the window.
            const auto dragY = ::GetSystemMetricsForDpi(SM_CYDRAG, dpi);
            const auto dragX = ::GetSystemMetricsForDpi(SM_CXDRAG, dpi);
            const auto xPos = _isMaximized ? _maximizedMargins.cxLeftWidth : dragX;
            const auto yPos = _isMaximized ? _maximizedMargins.cyTopHeight : dragY;

            // Create brush for borders, titlebar color.
            const auto backgroundBrush = _titlebar.Background();
            const auto backgroundSolidBrush = backgroundBrush.as<Media::SolidColorBrush>();
            const auto backgroundColor = backgroundSolidBrush.Color();
            const auto color = RGB(backgroundColor.R, backgroundColor.G, backgroundColor.B);
            _backgroundBrush = wil::unique_hbrush(CreateSolidBrush(color));

            RECT windowRect = {};
            ::GetWindowRect(_window.get(), &windowRect);
            const auto cx = windowRect.right - windowRect.left;
            const auto cy = windowRect.bottom - windowRect.top;

            // Fill in ONLY the titlebar area. If we paint the _entirety_ of the
            // window rect here, the single pixel of the bottom border (set in
            // _UpdateFrameMargins) will be drawn, and blend with whatever the
            // border color is.
            RECT dragBarRect = GetDragAreaRect();
            const auto dragHeight = RECT_HEIGHT(&dragBarRect);
            dragBarRect.left = 0;
            dragBarRect.right = cx;
            dragBarRect.top = 0;
            dragBarRect.bottom = dragHeight + yPos;
            ::FillRect(hdc.get(), &dragBarRect, _backgroundBrush.get());

            // Draw the top window border
            RECT clientRect = { 0, 0, cx, yPos };
            ::FillRect(hdc.get(), &clientRect, _backgroundBrush.get());

            // Draw the left window border
            clientRect = { 0, 0, xPos, cy };
            ::FillRect(hdc.get(), &clientRect, _backgroundBrush.get());

            // Draw the bottom window border
            clientRect = { 0, cy - yPos, cx, cy };
            ::FillRect(hdc.get(), &clientRect, _backgroundBrush.get());

            // Draw the right window border
            clientRect = { cx - xPos, 0, cx, cy };
            ::FillRect(hdc.get(), &clientRect, _backgroundBrush.get());
        }

        return 0;
    }

    case WM_NCLBUTTONDOWN:
    case WM_NCLBUTTONUP:
    case WM_NCMBUTTONDOWN:
    case WM_NCMBUTTONUP:
    case WM_NCRBUTTONDOWN:
    case WM_NCRBUTTONUP:
    case WM_NCXBUTTONDOWN:
    case WM_NCXBUTTONUP:
    {
        // If we clicked in the titlebar, raise an event so the app host can
        // dispatch an appropriate event.
        _DragRegionClickedHandlers();
        break;
    }

    case WM_WINDOWPOSCHANGING:
    {
        // Enforce maximum size here instead of WM_GETMINMAXINFO. If we return
        // it in WM_GETMINMAXINFO, then it will be enforced when snapping across
        // DPI boundaries (bad.)
        LPWINDOWPOS lpwpos = reinterpret_cast<LPWINDOWPOS>(lParam);
        if (lpwpos == nullptr)
        {
            break;
        }
        if (_HandleWindowPosChanging(lpwpos))
        {
            return 0;
        }
        else
        {
            break;
        }
    }
    case WM_DPICHANGED:
    {
        auto lprcNewScale = reinterpret_cast<RECT*>(lParam);
        OnSize(RECT_WIDTH(lprcNewScale), RECT_HEIGHT(lprcNewScale));
        break;
    }
    }

    return IslandWindow::MessageHandler(message, wParam, lParam);
}

// Method Description:
// - Handle a WM_ACTIVATE message. Called during the creation of the window, and
//   used as an opprotunity to get the dimensions of the caption buttons (the
//   min, max, close buttons). We'll use these dimensions to help size the
//   non-client area of the window.
void NonClientIslandWindow::_HandleActivateWindow()
{
    THROW_IF_FAILED(_UpdateFrameMargins());
}

// Method Description:
// - Handle a WM_WINDOWPOSCHANGING message. When the window is changing, or the
//   dpi is changing, this handler is triggered to give us a chance to adjust
//   the window size and position manually. We use this handler during a maxiize
//   to figure out by how much the window will overhang the edges of the
//   monitor, and set up some padding to adjust for that.
// Arguments:
// - windowPos: A pointer to a proposed window location and size. Should we wish
//   to manually position the window, we could change the values of this struct.
// Return Value:
// - true if we handled this message, false otherwise. If we return false, the
//   message should instead be handled by DefWindowProc
// Note:
// Largely taken from the conhost WM_WINDOWPOSCHANGING handler.
bool NonClientIslandWindow::_HandleWindowPosChanging(WINDOWPOS* const windowPos)
{
    // We only need to apply restrictions if the size is changing.
    if (WI_IsFlagSet(windowPos->flags, SWP_NOSIZE))
    {
        return false;
    }

    const auto windowStyle = GetWindowStyle(_window.get());
    const auto isMaximized = WI_IsFlagSet(windowStyle, WS_MAXIMIZE);
    const auto isIconified = WI_IsFlagSet(windowStyle, WS_ICONIC);

    if (_titlebar)
    {
        _titlebar.SetWindowVisualState(isMaximized ? winrt::TerminalApp::WindowVisualState::WindowVisualStateMaximized :
                                                     isIconified ? winrt::TerminalApp::WindowVisualState::WindowVisualStateIconified :
                                                                   winrt::TerminalApp::WindowVisualState::WindowVisualStateNormal);
    }

    // Figure out the suggested dimensions
    RECT rcSuggested;
    rcSuggested.left = windowPos->x;
    rcSuggested.top = windowPos->y;
    rcSuggested.right = rcSuggested.left + windowPos->cx;
    rcSuggested.bottom = rcSuggested.top + windowPos->cy;
    SIZE szSuggested;
    szSuggested.cx = RECT_WIDTH(&rcSuggested);
    szSuggested.cy = RECT_HEIGHT(&rcSuggested);

    // Figure out the current dimensions for comparison.
    RECT rcCurrent = GetWindowRect();

    // Determine whether we're being resized by someone dragging the edge or
    // completely moved around.
    bool fIsEdgeResize = false;
    {
        // We can only be edge resizing if our existing rectangle wasn't empty.
        // If it was empty, we're doing the initial create.
        if (!IsRectEmpty(&rcCurrent))
        {
            // If one or two sides are changing, we're being edge resized.
            unsigned int cSidesChanging = 0;
            if (rcCurrent.left != rcSuggested.left)
            {
                cSidesChanging++;
            }
            if (rcCurrent.right != rcSuggested.right)
            {
                cSidesChanging++;
            }
            if (rcCurrent.top != rcSuggested.top)
            {
                cSidesChanging++;
            }
            if (rcCurrent.bottom != rcSuggested.bottom)
            {
                cSidesChanging++;
            }

            if (cSidesChanging == 1 || cSidesChanging == 2)
            {
                fIsEdgeResize = true;
            }
        }
    }

    // If we're about to maximize the window, determine how much we're about to
    // overhang by, and adjust for that.
    // We need to do this because maximized windows will typically overhang the
    // actual monitor bounds by roughly the size of the old "thick: window
    // borders. For normal windows, this is fine, but because we're using
    // DwmExtendFrameIntoClientArea, that means some of our client content will
    // now overhang, and get cut off.
    if (isMaximized)
    {
        // Find the related monitor, the maximum pixel size,
        // and the dpi for the suggested rect.
        UINT dpiOfMaximum;
        RECT rcMaximum;

        if (fIsEdgeResize)
        {
            // If someone's dragging from the edge to resize in one direction,
            // we want to make sure we never grow past the current monitor.
            rcMaximum = GetMaxWindowRectInPixels(&rcCurrent, &dpiOfMaximum);
        }
        else
        {
            // In other circumstances, assume we're snapping around or some
            // other jump (TS). Just do whatever we're told using the new
            // suggestion as the restriction monitor.
            rcMaximum = GetMaxWindowRectInPixels(&rcSuggested, &dpiOfMaximum);
        }

        const auto suggestedWidth = szSuggested.cx;
        const auto suggestedHeight = szSuggested.cy;

        const auto maxWidth = RECT_WIDTH(&rcMaximum);
        const auto maxHeight = RECT_HEIGHT(&rcMaximum);

        // Only apply the maximum size restriction if the current DPI matches
        // the DPI of the maximum rect. This keeps us from applying the wrong
        // restriction if the monitor we're moving to has a different DPI but
        // we've yet to get notified of that DPI change. If we do apply it, then
        // we'll restrict the console window BEFORE its been resized for the DPI
        // change, so we're likely to shrink the window too much or worse yet,
        // keep it from moving entirely. We'll get a WM_DPICHANGED, resize the
        // window, and then process the restriction in a few window messages.
        if (((int)dpiOfMaximum == _currentDpi) &&
            ((suggestedWidth > maxWidth) ||
             (suggestedHeight > maxHeight)))
        {
            RECT frame{};
            // Calculate the maxmized window overhang by getting the size of the window frame.
            // We use the style without WS_CAPTION otherwise the caption height is included.
            // Only remove WS_DLGFRAME since WS_CAPTION = WS_DLGFRAME | WS_BORDER,
            // but WS_BORDER is needed as it modifies the calculation of the width of the frame.
            const auto targetStyle = windowStyle & ~WS_DLGFRAME;
            AdjustWindowRectExForDpi(&frame, targetStyle, false, GetWindowExStyle(_window.get()), _currentDpi);

            // Frame left and top will be negative
            _maximizedMargins.cxLeftWidth = frame.left * -1;
            _maximizedMargins.cyTopHeight = frame.top * -1;
            _maximizedMargins.cxRightWidth = frame.right;
            _maximizedMargins.cyBottomHeight = frame.bottom;

            _isMaximized = true;
            THROW_IF_FAILED(_UpdateFrameMargins());
        }
    }
    else
    {
        // Clear our maximization state
        _maximizedMargins = { 0 };

        // Immediately after resoring down, don't update our frame margins. If
        // you do this here, then a small gap will appear between the titlebar
        // and the content, until the window is moved. However, we do need to
        // keep this here _in general_ for dragging across DPI boundaries.
        if (!_isMaximized)
        {
            THROW_IF_FAILED(_UpdateFrameMargins());
        }

        _isMaximized = false;
    }
    return true;
}
