// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

#pragma once

// Custom window messages
#define CM_UPDATE_TITLE (WM_USER)

#include <wil/resource.h>

template<typename T>
class BaseWindow
{
public:
    virtual ~BaseWindow() = 0;
    static T* GetThisFromHandle(HWND const window) noexcept
    {
        return reinterpret_cast<T*>(GetWindowLongPtr(window, GWLP_USERDATA));
    }

    [[nodiscard]] static LRESULT __stdcall WndProc(HWND const window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept
    {
        WINRT_ASSERT(window);

        if (WM_NCCREATE == message)
        {
            auto cs = reinterpret_cast<CREATESTRUCT*>(lparam);
            T* that = static_cast<T*>(cs->lpCreateParams);
            WINRT_ASSERT(that);
            WINRT_ASSERT(!that->_window);
            that->_window = wil::unique_hwnd(window);

            return that->_OnNcCreate(wparam, lparam);
        }
        else if (T* that = GetThisFromHandle(window))
        {
            return that->MessageHandler(message, wparam, lparam);
        }

        return DefWindowProc(window, message, wparam, lparam);
    }

    [[nodiscard]] virtual LRESULT MessageHandler(UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept
    {
        switch (message)
        {
        case WM_DPICHANGED:
        {
            return HandleDpiChange(_window.get(), wparam, lparam);
        }

        case WM_DESTROY:
        {
            PostQuitMessage(0);
            return 0;
        }

        case WM_SIZE:
        {
            UINT width = LOWORD(lparam);
            UINT height = HIWORD(lparam);

            switch (wparam)
            {
            case SIZE_MAXIMIZED:
                [[fallthrough]];
            case SIZE_RESTORED:
                if (_minimized)
                {
                    _minimized = false;
                    OnRestore();
                }

                // We always need to fire the resize event, even when we're transitioning from minimized.
                // We might be transitioning directly from minimized to maximized, and we'll need
                // to trigger any size-related content changes.
                OnResize(width, height);
                break;
            case SIZE_MINIMIZED:
                if (!_minimized)
                {
                    _minimized = true;
                    OnMinimize();
                }
                break;
            default:
                // do nothing.
                break;
            }
            break;
        }
        case CM_UPDATE_TITLE:
        {
            SetWindowTextW(_window.get(), _title.c_str());
            break;
        }
        }

        return DefWindowProc(_window.get(), message, wparam, lparam);
    }

    // DPI Change handler. on WM_DPICHANGE resize the window
    [[nodiscard]] LRESULT HandleDpiChange(const HWND hWnd, const WPARAM wParam, const LPARAM lParam)
    {
        _inDpiChange = true;
        const HWND hWndStatic = GetWindow(hWnd, GW_CHILD);
        if (hWndStatic != nullptr)
        {
            const UINT uDpi = HIWORD(wParam);

            // Resize the window
            auto lprcNewScale = reinterpret_cast<RECT*>(lParam);

            SetWindowPos(hWnd, nullptr, lprcNewScale->left, lprcNewScale->top, lprcNewScale->right - lprcNewScale->left, lprcNewScale->bottom - lprcNewScale->top, SWP_NOZORDER | SWP_NOACTIVATE);

            _currentDpi = uDpi;
        }
        _inDpiChange = false;
        return 0;
    }

    virtual void OnResize(const UINT width, const UINT height) = 0;
    virtual void OnMinimize() = 0;
    virtual void OnRestore() = 0;

    RECT GetWindowRect() const noexcept
    {
        RECT rc = { 0 };
        ::GetWindowRect(_window.get(), &rc);
        return rc;
    }

    HWND GetHandle() const noexcept
    {
        return _window.get();
    }

    float GetCurrentDpiScale() const noexcept
    {
        const auto dpi = ::GetDpiForWindow(_window.get());
        const auto scale = static_cast<float>(dpi) / static_cast<float>(USER_DEFAULT_SCREEN_DPI);
        return scale;
    }

    // Gets the physical size of the client area of the HWND in _window
    SIZE GetPhysicalSize() const noexcept
    {
        RECT rect = {};
        GetClientRect(_window.get(), &rect);
        const auto windowsWidth = rect.right - rect.left;
        const auto windowsHeight = rect.bottom - rect.top;
        return SIZE{ windowsWidth, windowsHeight };
    }

    // Gets the logical (in DIPs) size of a physical size specified by the parameter physicalSize
    // Remarks:
    // XAML coordinate system is always in Display Independent Pixels (a.k.a DIPs or Logical). However Win32 GDI (because of legacy reasons)
    // in DPI mode "Per-Monitor and Per-Monitor (V2) DPI Awareness" is always in physical pixels.
    // The formula to transform is:
    //     logical = (physical / dpi) + 0.5 // 0.5 is to ensure that we pixel snap correctly at the edges, this is necessary with odd DPIs like 1.25, 1.5, 1, .75
    // See also:
    //   https://docs.microsoft.com/en-us/windows/desktop/LearnWin32/dpi-and-device-independent-pixels
    //   https://docs.microsoft.com/en-us/windows/desktop/hidpi/high-dpi-desktop-application-development-on-windows#per-monitor-and-per-monitor-v2-dpi-awareness
    winrt::Windows::Foundation::Size GetLogicalSize(const SIZE physicalSize) const noexcept
    {
        const auto scale = GetCurrentDpiScale();
        // 0.5 is to ensure that we pixel snap correctly at the edges, this is necessary with odd DPIs like 1.25, 1.5, 1, .75
        const auto logicalWidth = (physicalSize.cx / scale) + 0.5f;
        const auto logicalHeight = (physicalSize.cy / scale) + 0.5f;
        return winrt::Windows::Foundation::Size(logicalWidth, logicalHeight);
    }

    winrt::Windows::Foundation::Size GetLogicalSize() const noexcept
    {
        return GetLogicalSize(GetPhysicalSize());
    }

    // Method Description:
    // - Sends a message to our message loop to update the title of the window.
    // Arguments:
    // - newTitle: a string to use as the new title of the window.
    // Return Value:
    // - <none>
    void UpdateTitle(std::wstring_view newTitle)
    {
        _title = newTitle;
        PostMessageW(_window.get(), CM_UPDATE_TITLE, 0, reinterpret_cast<LPARAM>(nullptr));
    }

    // Method Description:
    // Reset the current dpi of the window. This method is only called after we change the
    // initial launch position. This makes sure the dpi is consistent with the monitor on which
    // the window will launch
    void RefreshCurrentDPI()
    {
        _currentDpi = GetDpiForWindow(_window.get());
    }

protected:
    using base_type = BaseWindow<T>;
    wil::unique_hwnd _window;

    unsigned int _currentDpi = 0;
    bool _inDpiChange = false;

    std::wstring _title = L"";

    bool _minimized = false;

    // Method Description:
    // - This method is called when the window receives the WM_NCCREATE message.
    // Return Value:
    // - The value returned from the window proc.
    virtual [[nodiscard]] LRESULT _OnNcCreate(WPARAM wParam, LPARAM lParam) noexcept
    {
        SetWindowLongPtr(_window.get(), GWLP_USERDATA, reinterpret_cast<LONG_PTR>(this));

        EnableNonClientDpiScaling(_window.get());
        _currentDpi = GetDpiForWindow(_window.get());

        return DefWindowProc(_window.get(), WM_NCCREATE, wParam, lParam);
    };
};

template<typename T>
inline BaseWindow<T>::~BaseWindow()
{
}
