/*****************************************************************************
 * Copyright (c) 2014-2025 OpenRCT2 developers
 *
 * For a complete list of all authors, please refer to contributors.md
 * Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2
 *
 * OpenRCT2 is licensed under the GNU General Public License version 3.
 *****************************************************************************/

#include "Window.h"

#include "../UiStringIds.h"
#include "Theme.h"
#include "Widget.h"

#include <SDL.h>
#include <algorithm>
#include <openrct2-ui/windows/Windows.h>
#include <openrct2/Context.h>
#include <openrct2/GameState.h>
#include <openrct2/Input.h>
#include <openrct2/OpenRCT2.h>
#include <openrct2/SpriteIds.h>
#include <openrct2/audio/Audio.h>
#include <openrct2/config/Config.h>
#include <openrct2/drawing/Drawing.h>
#include <openrct2/entity/EntityRegistry.h>
#include <openrct2/interface/Viewport.h>
#include <openrct2/interface/Widget.h>
#include <openrct2/ui/WindowManager.h>
#include <openrct2/world/Location.hpp>

class Formatter;

namespace OpenRCT2
{
    using namespace OpenRCT2::Ui;

    // The amount of pixels to scroll per wheel click
    constexpr int32_t kWindowScrollPixels = 17;

    static int32_t _previousAbsoluteWheel = 0;

    static int32_t WindowGetWidgetIndex(const WindowBase& w, Widget* widget)
    {
        const auto it = std::find_if(
            w.widgets.begin(), w.widgets.end(), [&](auto& otherWidget) { return &otherWidget == widget; });
        if (it == w.widgets.end())
            return -1;
        return std::distance(w.widgets.begin(), it);
    }

    static int32_t WindowGetScrollIndex(const WindowBase& w, int32_t targetWidgetIndex)
    {
        if (w.widgets[targetWidgetIndex].type != WidgetType::scroll)
            return -1;

        int32_t scrollIndex = 0;
        for (WidgetIndex widgetIndex = 0; widgetIndex < w.widgets.size(); widgetIndex++)
        {
            if (widgetIndex == targetWidgetIndex)
                break;
            auto& widget = w.widgets[widgetIndex];
            if (widget.type == WidgetType::scroll)
                scrollIndex++;
        }

        return scrollIndex;
    }

    static Widget* WindowGetScrollWidget(const WindowBase& w, int32_t scrollIndex)
    {
        for (WidgetIndex widgetIndex = 0; widgetIndex < w.widgets.size(); widgetIndex++)
        {
            auto& widget = w.widgets[widgetIndex];
            if (widget.type != WidgetType::scroll)
                continue;

            if (scrollIndex == 0)
                return const_cast<Widget*>(&widget);
            scrollIndex--;
        }

        return nullptr;
    }

    /**
     *
     *  rct2: 0x006E78E3
     */
    static void WindowScrollWheelInput(WindowBase& w, int32_t scrollIndex, int32_t wheel)
    {
        auto& scroll = w.scrolls[scrollIndex];
        Widget* widget = WindowGetScrollWidget(w, scrollIndex);
        WidgetIndex widgetIndex = WindowGetWidgetIndex(w, widget);

        if (scroll.flags & VSCROLLBAR_VISIBLE)
        {
            int32_t size = widget->height() - 1;
            if (scroll.flags & HSCROLLBAR_VISIBLE)
                size -= 11;
            size = std::max(0, scroll.contentHeight - size);
            scroll.contentOffsetY = std::min(std::max(0, scroll.contentOffsetY + wheel), size);
        }
        else
        {
            int32_t size = widget->width() - 1;
            if (scroll.flags & VSCROLLBAR_VISIBLE)
                size -= 11;
            size = std::max(0, scroll.contentWidth - size);
            scroll.contentOffsetX = std::min(std::max(0, scroll.contentOffsetX + wheel), size);
        }

        widgetScrollUpdateThumbs(w, widgetIndex);

        auto* windowMgr = Ui::GetWindowManager();
        windowMgr->InvalidateWidget(w, widgetIndex);
    }

    /**
     *
     *  rct2: 0x006E793B
     */
    static int32_t WindowWheelInput(WindowBase& w, int32_t wheel)
    {
        int32_t scrollIndex = 0;
        for (WidgetIndex widgetIndex = 0; widgetIndex < w.widgets.size(); widgetIndex++)
        {
            const auto& widget = w.widgets[widgetIndex];
            if (widget.type != WidgetType::scroll)
                continue;

            // Originally always checked first scroll view, bug maybe?
            const auto& scroll = w.scrolls[scrollIndex];
            if (scroll.flags & (HSCROLLBAR_VISIBLE | VSCROLLBAR_VISIBLE))
            {
                WindowScrollWheelInput(w, scrollIndex, wheel);
                return 1;
            }
            scrollIndex++;
        }

        return 0;
    }

    /**
     *
     *  rct2: 0x006E79FB
     */
    static void WindowViewportWheelInput(WindowBase& w, int32_t wheel)
    {
        if (gLegacyScene == LegacyScene::trackDesignsManager || gLegacyScene == LegacyScene::titleSequence)
            return;

        if (wheel < 0)
            Windows::WindowZoomIn(w, true);
        else if (wheel > 0)
            Windows::WindowZoomOut(w, true);
    }

    static bool isSpinnerGroup(WindowBase& w, WidgetIndex index, WidgetType buttonType)
    {
        const auto& widgets = w.widgets;

        if (widgets[index].type != WidgetType::spinner && widgets[index].type != WidgetType::imgBtn)
            return false;

        if (static_cast<size_t>(index + 2) >= widgets.size())
            return false;

        if (widgets[index + 1].type != buttonType)
            return false;

        if (widgets[index + 2].type != buttonType)
            return false;

        return true;
    }

    static std::optional<WidgetIndex> getSpinnerGroupWidgetIndex(WindowBase& w, WidgetIndex startIndex)
    {
        // We only iterate 3 times as we might be at the spinner or one of its buttons.
        for (WidgetIndex index = 0; index < 3; index++)
        {
            const auto reverseIndex = startIndex - index;
            if (reverseIndex < 0)
            {
                break;
            }

            if (isSpinnerGroup(w, reverseIndex, WidgetType::trnBtn))
            {
                return reverseIndex;
            }

            if (isSpinnerGroup(w, reverseIndex, WidgetType::button))
            {
                return reverseIndex;
            }
        }

        return std::nullopt;
    }

    // Allow mouse wheel scrolling to manipulate spinner widgets and tool sizes
    static bool WindowOtherWheelInput(WindowBase& w, WidgetIndex widgetIndex, int32_t wheel)
    {
        // HACK: Until we have a new window system that allows us to add new events like mouse wheel easily,
        //       this selective approach will have to do.

        const auto spinnerGroupIndex = getSpinnerGroupWidgetIndex(w, widgetIndex);
        if (!spinnerGroupIndex.has_value())
        {
            return false;
        }

        const auto entryWidgetType = w.widgets[*spinnerGroupIndex].type;
        auto targetWidgetIndex = *spinnerGroupIndex;

        if (entryWidgetType == WidgetType::imgBtn)
        {
            auto expectedContent1 = ImageId(SPR_LAND_TOOL_DECREASE, FilterPaletteID::PaletteNull);
            auto expectedContent2 = ImageId(SPR_LAND_TOOL_INCREASE, FilterPaletteID::PaletteNull);

            auto button1Image = w.widgets[*spinnerGroupIndex + 1].image;
            auto button2Image = w.widgets[*spinnerGroupIndex + 2].image;
            if (button1Image != expectedContent1 || button2Image != expectedContent2)
            {
                return false;
            }

            // Expected widget order: decrease, increase
            targetWidgetIndex += wheel < 0 ? 2 : 1;
        }
        else if (entryWidgetType == WidgetType::spinner)
        {
            auto button1StringId = w.widgets[*spinnerGroupIndex + 1].text;
            auto button2StringId = w.widgets[*spinnerGroupIndex + 2].text;
            if (button1StringId != STR_NUMERIC_UP || button2StringId != STR_NUMERIC_DOWN)
            {
                return false;
            }

            // Expected widget order: increase, decrease
            targetWidgetIndex += wheel < 0 ? 1 : 2;
        }

        assert(targetWidgetIndex >= 0 && targetWidgetIndex < w.widgets.size());

        if (widgetIsDisabled(w, targetWidgetIndex))
        {
            return false;
        }

        w.OnMouseDown(targetWidgetIndex);
        return true;
    }

    /**
     *
     *  rct2: 0x006E7868
     */
    void WindowAllWheelInput()
    {
        // Get wheel value
        auto cursorState = ContextGetCursorState();
        int32_t absolute_wheel = cursorState->wheel;
        int32_t relative_wheel = absolute_wheel - _previousAbsoluteWheel;
        int32_t pixel_scroll = relative_wheel * kWindowScrollPixels;
        _previousAbsoluteWheel = absolute_wheel;

        if (relative_wheel == 0)
            return;

        // Check window cursor is over
        if (!gInputFlags.has(InputFlag::unk5))
        {
            auto* windowMgr = GetWindowManager();
            WindowBase* w = windowMgr->FindFromPoint(cursorState->position);
            if (w != nullptr)
            {
                // Check if main window
                if (w->classification == WindowClass::MainWindow || w->classification == WindowClass::Viewport)
                {
                    WindowViewportWheelInput(*w, relative_wheel);
                    return;
                }

                // Check scroll view, cursor is over
                WidgetIndex widgetIndex = windowMgr->FindWidgetFromPoint(*w, cursorState->position);
                if (widgetIndex != kWidgetIndexNull)
                {
                    const auto& widget = w->widgets[widgetIndex];
                    if (widget.type == WidgetType::scroll)
                    {
                        int32_t scrollIndex = WindowGetScrollIndex(*w, widgetIndex);
                        const auto& scroll = w->scrolls[scrollIndex];
                        if (scroll.flags & (HSCROLLBAR_VISIBLE | VSCROLLBAR_VISIBLE))
                        {
                            WindowScrollWheelInput(*w, WindowGetScrollIndex(*w, widgetIndex), pixel_scroll);
                            return;
                        }
                    }
                    else
                    {
                        if (WindowOtherWheelInput(*w, widgetIndex, pixel_scroll))
                        {
                            return;
                        }
                    }

                    // Check other scroll views on window
                    if (WindowWheelInput(*w, pixel_scroll))
                        return;
                }
            }
        }
    }

    void ApplyScreenSaverLockSetting()
    {
        Config::Get().general.DisableScreensaver ? SDL_DisableScreenSaver() : SDL_EnableScreenSaver();
    }

    void Window::ScrollToViewport()
    {
        if (viewport == nullptr || !focus.has_value())
            return;

        CoordsXYZ newCoords = focus.value().GetPos();

        auto mainWindow = WindowGetMain();
        if (mainWindow != nullptr)
            WindowScrollToLocation(*mainWindow, newCoords);
    }

    void Window::OnDraw(RenderTarget& rt)
    {
        Windows::WindowDrawWidgets(*this, rt);
    }

    void Window::OnDrawWidget(WidgetIndex widgetIndex, RenderTarget& rt)
    {
        widgetDraw(rt, *this, widgetIndex);
    }

    void Window::InitScrollWidgets()
    {
        Windows::WindowInitScrollWidgets(*this);
    }

    void Window::InvalidateWidget(WidgetIndex widgetIndex)
    {
        auto* windowMgr = Ui::GetWindowManager();
        windowMgr->InvalidateWidget(*this, widgetIndex);
    }

    bool Window::IsWidgetDisabled(WidgetIndex widgetIndex) const
    {
        return widgetIsDisabled(*this, widgetIndex);
    }

    bool Window::IsWidgetPressed(WidgetIndex widgetIndex) const
    {
        return widgetIsPressed(*this, widgetIndex);
    }

    void Window::SetWidgetEnabled(WidgetIndex widgetIndex, bool value)
    {
        widgetSetEnabled(*this, widgetIndex, value);
    }

    void Window::SetWidgetDisabled(WidgetIndex widgetIndex, bool value)
    {
        widgetSetDisabled(*this, widgetIndex, value);
    }

    void Window::SetWidgetDisabledAndInvalidate(WidgetIndex widgetIndex, bool value)
    {
        bool oldState = IsWidgetDisabled(widgetIndex);
        if (oldState != value)
        {
            widgetSetDisabled(*this, widgetIndex, value);
            InvalidateWidget(widgetIndex);
        }
    }

    void Window::SetWidgetPressed(WidgetIndex widgetIndex, bool value)
    {
        widgetSetPressed(*this, widgetIndex, value);
    }

    void Window::SetCheckboxValue(WidgetIndex widgetIndex, bool value)
    {
        SetWidgetPressed(widgetIndex, value);
    }

    void Window::DrawWidgets(RenderTarget& rt)
    {
        Windows::WindowDrawWidgets(*this, rt);
    }

    void Window::Close()
    {
        CloseWindowModifier modifier = GetCloseModifier();

        if (modifier == CloseWindowModifier::shift)
        {
            CloseOthers();
        }
        else if (modifier == CloseWindowModifier::control)
        {
            CloseOthersOfThisClass();
        }
        else
        {
            auto* windowMgr = Ui::GetWindowManager();
            windowMgr->Close(*this);
        }
    }

    void Window::CloseOthers()
    {
        auto* windowMgr = Ui::GetWindowManager();
        windowMgr->CloseAllExceptNumberAndClass(number, classification);
    }

    void Window::CloseOthersOfThisClass()
    {
        auto* windowMgr = Ui::GetWindowManager();
        windowMgr->CloseByClass(classification);
    }

    CloseWindowModifier Window::GetCloseModifier()
    {
        CloseWindowModifier lastModifier = CloseWindowModifier::none;

        if (gLastCloseModifier.window.number == number && gLastCloseModifier.window.classification == classification)
        {
            lastModifier = gLastCloseModifier.modifier;
        }

        gLastCloseModifier.modifier = CloseWindowModifier::none;

        return lastModifier;
    }

    void Window::TextInputOpen(
        WidgetIndex callWidget, StringId title, StringId description, const Formatter& descriptionArgs, StringId existingText,
        uintptr_t existingArgs, int32_t maxLength)
    {
        OpenRCT2::Ui::Windows::WindowTextInputOpen(
            this, callWidget, title, description, descriptionArgs, existingText, existingArgs, maxLength);
    }

    void Window::ResizeSpinner(WidgetIndex widgetIndex, const ScreenCoordsXY& origin, const ScreenSize& size)
    {
        auto right = origin.x + size.width - 1;
        auto bottom = origin.y + size.height - 1;
        widgets[widgetIndex].left = origin.x;
        widgets[widgetIndex].top = origin.y;
        widgets[widgetIndex].right = right;
        widgets[widgetIndex].bottom = bottom;

        widgets[widgetIndex + 1].left = right - size.height; // subtract height to maintain aspect ratio
        widgets[widgetIndex + 1].top = origin.y + 1;
        widgets[widgetIndex + 1].right = right - 1;
        widgets[widgetIndex + 1].bottom = bottom - 1;

        widgets[widgetIndex + 2].left = right - size.height * 2;
        widgets[widgetIndex + 2].top = origin.y + 1;
        widgets[widgetIndex + 2].right = right - size.height - 1;
        widgets[widgetIndex + 2].bottom = bottom - 1;
    }

    void Window::ResizeDropdown(WidgetIndex widgetIndex, const ScreenCoordsXY& origin, const ScreenSize& size)
    {
        auto right = origin.x + size.width - 1;
        auto bottom = origin.y + size.height - 1;
        widgets[widgetIndex].left = origin.x;
        widgets[widgetIndex].top = origin.y;
        widgets[widgetIndex].right = right;
        widgets[widgetIndex].bottom = bottom;

        widgets[widgetIndex + 1].left = right - size.height + 1; // subtract height to maintain aspect ratio
        widgets[widgetIndex + 1].top = origin.y + 1;
        widgets[widgetIndex + 1].right = right - 1;
        widgets[widgetIndex + 1].bottom = bottom - 1;
    }

    void WindowAlignTabs(WindowBase* w, WidgetIndex start_tab_id, WidgetIndex end_tab_id)
    {
        assert(start_tab_id < w->widgets.size());
        assert(end_tab_id < w->widgets.size());

        int32_t i, x = w->widgets[start_tab_id].left;
        int32_t tab_width = w->widgets[start_tab_id].width();

        for (i = start_tab_id; i <= end_tab_id; i++)
        {
            auto& widget = w->widgets[i];
            assert(widget.type == WidgetType::tab);

            if (!widgetIsDisabled(*w, i))
            {
                widget.left = x;
                widget.right = x + tab_width;
                x += tab_width + 1;
            }
            else
            {
                // Workaround #20535: Avoid disabled widgets from sharing the same space as active ones, otherwise
                // WindowFindWidgetFromPoint could return the disabled one, causing issues.
                widget.left = 0;
                widget.right = 0;
            }
        }
    }

    ScreenCoordsXY WindowGetViewportSoundIconPos(WindowBase& w)
    {
        const uint8_t buttonOffset = (Config::Get().interface.WindowButtonsOnTheLeft) ? kCloseButtonSize + 2 : 0;
        return w.windowPos + ScreenCoordsXY{ 2 + buttonOffset, 2 };
    }
} // namespace OpenRCT2

namespace OpenRCT2::Ui::Windows
{
    static u8string _textBoxInput;
    static int32_t _textBoxFrameNo = 0;
    static bool _usingWidgetTextBox = false;
    static TextInputSession* _textInput;
    static WidgetIdentifier _currentTextBox = { { WindowClass::Null, 0 }, 0 };

    WindowBase* WindowGetListening()
    {
        for (auto it = g_window_list.rbegin(); it != g_window_list.rend(); it++)
        {
            auto& w = **it;
            if (w.flags & WF_DEAD)
                continue;

            auto viewport = w.viewport;
            if (viewport != nullptr)
            {
                if (viewport->flags & VIEWPORT_FLAG_SOUND_ON)
                {
                    return &w;
                }
            }
        }
        return nullptr;
    }

    WindowClass WindowGetClassification(const WindowBase& window)
    {
        return window.classification;
    }

    void WindowStartTextbox(const WindowBase& callW, WidgetIndex callWidget, u8string existingText, int32_t maxLength)
    {
        if (_usingWidgetTextBox)
            WindowCancelTextbox();

        _usingWidgetTextBox = true;
        _currentTextBox.window.classification = callW.classification;
        _currentTextBox.window.number = callW.number;
        _currentTextBox.widget_index = callWidget;
        _textBoxFrameNo = 0;

        auto* windowMgr = Ui::GetWindowManager();
        windowMgr->CloseByClass(WindowClass::Textinput);

        _textBoxInput = existingText;

        _textInput = ContextStartTextInput(_textBoxInput, maxLength);
    }

    void WindowCancelTextbox()
    {
        if (_usingWidgetTextBox)
        {
            auto* windowMgr = GetWindowManager();
            WindowBase* w = windowMgr->FindByNumber(_currentTextBox.window.classification, _currentTextBox.window.number);
            _currentTextBox.window.classification = WindowClass::Null;
            _currentTextBox.window.number = 0;
            ContextStopTextInput();
            _usingWidgetTextBox = false;
            if (w != nullptr)
            {
                windowMgr->InvalidateWidget(*w, _currentTextBox.widget_index);
            }
            _currentTextBox.widget_index = kWidgetIndexNull;
        }
    }

    void WindowUpdateTextboxCaret()
    {
        _textBoxFrameNo++;
        if (_textBoxFrameNo > 30)
            _textBoxFrameNo = 0;
    }

    void WindowUpdateTextbox()
    {
        if (_usingWidgetTextBox)
        {
            _textBoxFrameNo = 0;
            auto* windowMgr = GetWindowManager();
            WindowBase* w = windowMgr->FindByNumber(_currentTextBox.window.classification, _currentTextBox.window.number);
            windowMgr->InvalidateWidget(*w, _currentTextBox.widget_index);
            w->OnTextInput(_currentTextBox.widget_index, _textBoxInput);
        }
    }
    const TextInputSession* GetTextboxSession()
    {
        return _textInput;
    }
    void SetTexboxSession(TextInputSession* session)
    {
        _textInput = session;
    }
    bool IsUsingWidgetTextBox()
    {
        return _usingWidgetTextBox;
    }
    bool TextBoxCaretIsFlashed()
    {
        return _textBoxFrameNo <= 15;
    }
    const WidgetIdentifier& GetCurrentTextBox()
    {
        return _currentTextBox;
    }

    void WindowResizeByDelta(WindowBase& w, int16_t dw, int16_t dh)
    {
        if (dw == 0 && dh == 0)
            return;

        // Invalidate old region
        w.Invalidate();

        // Clamp new size to minimum and maximum
        w.width = std::clamp<int16_t>(w.width + dw, w.min_width, w.max_width);
        w.height = std::clamp<int16_t>(w.height + dh, w.min_height, w.max_height);

        w.OnResize();
        w.ResizeFrame();
        w.OnPrepareDraw();

        // Update scroll widgets
        for (auto& scroll : w.scrolls)
        {
            scroll.contentWidth = -1;
            scroll.contentHeight = -1;
        }
        WindowUpdateScrollWidgets(w);

        // Invalidate new region
        w.Invalidate();
    }

    /**
     *
     *  rct2: 0x006EAE4E
     *
     * @param w The window (esi).
     */
    void WindowUpdateScrollWidgets(WindowBase& w)
    {
        int32_t scrollIndex, width, height, scrollPositionChanged;
        scrollIndex = 0;
        for (WidgetIndex widgetIndex = 0; widgetIndex < w.widgets.size(); widgetIndex++)
        {
            auto& widget = w.widgets[widgetIndex];
            if (widget.type != WidgetType::scroll)
                continue;

            auto& scroll = w.scrolls[scrollIndex];
            ScreenSize scrollSize = w.OnScrollGetSize(scrollIndex);
            width = scrollSize.width;
            height = scrollSize.height;

            if (height == 0)
            {
                scroll.contentOffsetY = 0;
            }
            else if (width == 0)
            {
                scroll.contentOffsetX = 0;
            }
            width++;
            height++;

            scrollPositionChanged = 0;
            if ((widget.content & SCROLL_HORIZONTAL) && width != scroll.contentWidth)
            {
                scrollPositionChanged = 1;
                scroll.contentWidth = width;
            }

            if ((widget.content & SCROLL_VERTICAL) && height != scroll.contentHeight)
            {
                scrollPositionChanged = 1;
                scroll.contentHeight = height;
            }

            if (scrollPositionChanged)
            {
                widgetScrollUpdateThumbs(w, widgetIndex);
                w.Invalidate();
            }
            scrollIndex++;
        }
    }
    /**
     * Initialises scroll widgets to their virtual size.
     *  rct2: 0x006EAEB8
     */
    void WindowInitScrollWidgets(WindowBase& w)
    {
        int32_t scroll_index{};
        for (WidgetIndex widgetIndex = 0; widgetIndex < w.widgets.size(); widgetIndex++)
        {
            auto& widget = w.widgets[widgetIndex];
            if (widget.type != WidgetType::scroll)
            {
                continue;
            }

            auto& scroll = w.scrolls[scroll_index];
            scroll.flags = 0;
            ScreenSize scrollSize = w.OnScrollGetSize(scroll_index);
            scroll.contentOffsetX = 0;
            scroll.contentWidth = scrollSize.width + 1;
            scroll.contentOffsetY = 0;
            scroll.contentHeight = scrollSize.height + 1;

            if (widget.content & SCROLL_HORIZONTAL)
                scroll.flags |= HSCROLLBAR_VISIBLE;
            if (widget.content & SCROLL_VERTICAL)
                scroll.flags |= VSCROLLBAR_VISIBLE;

            widgetScrollUpdateThumbs(w, widgetIndex);
            scroll_index++;
        }
    }

    static void SnapLeft(WindowBase& w, int32_t proximity)
    {
        const auto* mainWindow = WindowGetMain();
        auto wBottom = w.windowPos.y + w.height;
        auto wLeftProximity = w.windowPos.x - (proximity * 2);
        auto wRightProximity = w.windowPos.x + (proximity * 2);
        auto rightMost = INT32_MIN;

        WindowVisitEach([&](WindowBase* w2) {
            if (w2 == &w || w2 == mainWindow)
                return;

            auto right = w2->windowPos.x + w2->width;

            if (wBottom < w2->windowPos.y || w.windowPos.y > w2->windowPos.y + w2->height)
                return;

            if (right < wLeftProximity || right > wRightProximity)
                return;

            rightMost = std::max(rightMost, right);
        });

        if (0 >= wLeftProximity && 0 <= wRightProximity)
            rightMost = std::max(rightMost, 0);

        if (rightMost != INT32_MIN)
            w.windowPos.x = rightMost;
    }

    static void SnapTop(WindowBase& w, int32_t proximity)
    {
        const auto* mainWindow = WindowGetMain();
        auto wRight = w.windowPos.x + w.width;
        auto wTopProximity = w.windowPos.y - (proximity * 2);
        auto wBottomProximity = w.windowPos.y + (proximity * 2);
        auto bottomMost = INT32_MIN;

        WindowVisitEach([&](WindowBase* w2) {
            if (w2 == &w || w2 == mainWindow)
                return;

            auto bottom = w2->windowPos.y + w2->height;

            if (wRight < w2->windowPos.x || w.windowPos.x > w2->windowPos.x + w2->width)
                return;

            if (bottom < wTopProximity || bottom > wBottomProximity)
                return;

            bottomMost = std::max(bottomMost, bottom);
        });

        if (0 >= wTopProximity && 0 <= wBottomProximity)
            bottomMost = std::max(bottomMost, 0);

        if (bottomMost != INT32_MIN)
            w.windowPos.y = bottomMost;
    }

    static void SnapRight(WindowBase& w, int32_t proximity)
    {
        const auto* mainWindow = WindowGetMain();
        auto wRight = w.windowPos.x + w.width;
        auto wBottom = w.windowPos.y + w.height;
        auto wLeftProximity = wRight - (proximity * 2);
        auto wRightProximity = wRight + (proximity * 2);
        auto leftMost = INT32_MAX;

        WindowVisitEach([&](WindowBase* w2) {
            if (w2 == &w || w2 == mainWindow)
                return;

            if (wBottom < w2->windowPos.y || w.windowPos.y > w2->windowPos.y + w2->height)
                return;

            if (w2->windowPos.x < wLeftProximity || w2->windowPos.x > wRightProximity)
                return;

            leftMost = std::min<int32_t>(leftMost, w2->windowPos.x);
        });

        auto screenWidth = ContextGetWidth();
        if (screenWidth >= wLeftProximity && screenWidth <= wRightProximity)
            leftMost = std::min(leftMost, screenWidth);

        if (leftMost != INT32_MAX)
            w.windowPos.x = leftMost - w.width;
    }

    static void SnapBottom(WindowBase& w, int32_t proximity)
    {
        const auto* mainWindow = WindowGetMain();
        auto wRight = w.windowPos.x + w.width;
        auto wBottom = w.windowPos.y + w.height;
        auto wTopProximity = wBottom - (proximity * 2);
        auto wBottomProximity = wBottom + (proximity * 2);
        auto topMost = INT32_MAX;

        WindowVisitEach([&](WindowBase* w2) {
            if (w2 == &w || w2 == mainWindow)
                return;

            if (wRight < w2->windowPos.x || w.windowPos.x > w2->windowPos.x + w2->width)
                return;

            if (w2->windowPos.y < wTopProximity || w2->windowPos.y > wBottomProximity)
                return;

            topMost = std::min<int32_t>(topMost, w2->windowPos.y);
        });

        auto screenHeight = ContextGetHeight();
        if (screenHeight >= wTopProximity && screenHeight <= wBottomProximity)
            topMost = std::min(topMost, screenHeight);

        if (topMost != INT32_MAX)
            w.windowPos.y = topMost - w.height;
    }

    void WindowMoveAndSnap(WindowBase& w, ScreenCoordsXY newWindowCoords, int32_t snapProximity)
    {
        auto originalPos = w.windowPos;
        int32_t minY = gLegacyScene == LegacyScene::titleSequence ? 1 : kTopToolbarHeight + 2;

        newWindowCoords.y = std::clamp(newWindowCoords.y, minY, ContextGetHeight() - 34);

        if (snapProximity > 0)
        {
            w.windowPos = newWindowCoords;

            SnapRight(w, snapProximity);
            SnapBottom(w, snapProximity);
            SnapLeft(w, snapProximity);
            SnapTop(w, snapProximity);

            if (w.windowPos == originalPos)
                return;

            newWindowCoords = w.windowPos;
            w.windowPos = originalPos;
        }

        WindowSetPosition(w, newWindowCoords);
    }

    void WindowMovePosition(WindowBase& w, const ScreenCoordsXY& deltaCoords)
    {
        if (deltaCoords.x == 0 && deltaCoords.y == 0)
            return;

        // Invalidate old region
        w.Invalidate();

        // Translate window and viewport
        w.windowPos += deltaCoords;
        if (w.viewport != nullptr)
        {
            w.viewport->pos += deltaCoords;
        }

        // Invalidate new region
        w.Invalidate();
    }

    void WindowSetPosition(WindowBase& w, const ScreenCoordsXY& screenCoords)
    {
        WindowMovePosition(w, screenCoords - w.windowPos);
    }

    /**
     *
     *  rct2: 0x006ED710
     * Called after a window resize to move windows if they
     * are going to be out of sight.
     */
    void WindowRelocateWindows(int32_t width, int32_t height)
    {
        int32_t new_location = 8;
        WindowVisitEach([width, height, &new_location](WindowBase* w) {
            // Work out if the window requires moving
            if (w->windowPos.x + 10 < width)
            {
                if (w->flags & (WF_STICK_TO_BACK | WF_STICK_TO_FRONT))
                {
                    if (w->windowPos.y - 22 < height)
                    {
                        return;
                    }
                }
                if (w->windowPos.y + 10 < height)
                {
                    return;
                }
            }

            // Calculate the new locations
            auto newWinPos = w->windowPos;
            w->windowPos = { new_location, new_location + kTopToolbarHeight + 1 };

            // Move the next new location so windows are not directly on top
            new_location += 8;

            // Adjust the viewport if required.
            if (w->viewport != nullptr)
            {
                w->viewport->pos -= newWinPos - w->windowPos;
            }
        });
    }

    bool WindowSetResize(WindowBase& w, ScreenSize minSize, ScreenSize maxSize)
    {
        w.min_width = std::min(minSize.width, maxSize.width);
        w.min_height = std::min(minSize.height, maxSize.height);
        w.max_width = std::max(minSize.width, maxSize.width);
        w.max_height = std::max(minSize.height, maxSize.height);

        if (Config::Get().interface.EnlargedUi)
        {
            // Not sure why plugin windows have to be treated differently,
            // but they currently show a deviation if we don't.
            if (w.classification == WindowClass::Custom)
            {
                w.min_height += w.getTitleBarDiffTarget();
                w.max_height += w.getTitleBarDiffTarget();
            }
            else
            {
                w.min_height += w.getTitleBarDiffNormal();
                w.max_height += w.getTitleBarDiffNormal();
            }
        }

        // Clamp width and height to minimum and maximum
        int16_t width = std::clamp<int16_t>(w.width, w.min_width, w.max_width);
        int16_t height = std::clamp<int16_t>(w.height, w.min_height, w.max_height);

        // Resize window if size has changed
        if (w.width != width || w.height != height)
        {
            w.Invalidate();
            w.width = width;
            w.height = height;
            w.ResizeFrame();
            w.Invalidate();
            return true;
        }

        return false;
    }

    bool WindowCanResize(const WindowBase& w)
    {
        return (w.flags & WF_RESIZABLE) && (w.min_width != w.max_width || w.min_height != w.max_height);
    }

    /**
     *
     *  rct2: 0x006EA73F
     */
    void InvalidateAllWindowsAfterInput()
    {
        WindowVisitEach([](WindowBase* w) { Windows::WindowUpdateScrollWidgets(*w); });
    }

    /**
     *
     *  rct2: 0x00685BE1
     *
     * @param dpi (edi)
     * @param w (esi)
     */
    void WindowDrawViewport(RenderTarget& rt, WindowBase& w)
    {
        ViewportRender(rt, w.viewport);
    }

    /**
     *
     *  rct2: 0x006EB15C
     */
    void WindowDrawWidgets(WindowBase& w, RenderTarget& rt)
    {
        if ((w.flags & WF_TRANSPARENT) && !(w.flags & WF_NO_BACKGROUND))
            GfxFilterRect(
                rt, { w.windowPos, w.windowPos + ScreenCoordsXY{ w.width - 1, w.height - 1 } }, FilterPaletteID::Palette51);

        // todo: some code missing here? Between 006EB18C and 006EB260
        for (WidgetIndex widgetIndex = 0; widgetIndex < w.widgets.size(); widgetIndex++)
        {
            auto& widget = w.widgets[widgetIndex];
            if (!widget.IsVisible())
            {
                continue;
            }

            // Check if widget is outside the draw region
            if (w.windowPos.x + widget.left < rt.x + rt.width && w.windowPos.x + widget.right >= rt.x)
            {
                if (w.windowPos.y + widget.top < rt.y + rt.height && w.windowPos.y + widget.bottom >= rt.y)
                {
                    w.OnDrawWidget(widgetIndex, rt);
                }
            }
        }

        // todo: something missing here too? Between 006EC32B and 006EC369

        if (w.flags & WF_WHITE_BORDER_MASK)
        {
            GfxFillRectInset(
                rt, { w.windowPos, w.windowPos + ScreenCoordsXY{ w.width - 1, w.height - 1 } }, { COLOUR_WHITE },
                INSET_RECT_FLAG_FILL_NONE);
        }
    }

    /**
     *
     *  rct2: 0x006887A6
     */
    void WindowZoomIn(WindowBase& w, bool atCursor)
    {
        WindowZoomSet(w, w.viewport->zoom - 1, atCursor);
    }

    /**
     *
     *  rct2: 0x006887E0
     */
    void WindowZoomOut(WindowBase& w, bool atCursor)
    {
        WindowZoomSet(w, w.viewport->zoom + 1, atCursor);
    }

    void MainWindowZoom(bool zoomIn, bool atCursor)
    {
        auto* mainWindow = WindowGetMain();
        if (mainWindow == nullptr)
            return;

        if (gLegacyScene == LegacyScene::titleSequence)
            return;

        if (gLegacyScene == LegacyScene::scenarioEditor && getGameState().editorStep != EditorStep::LandscapeEditor)
            return;

        if (gLegacyScene == LegacyScene::trackDesignsManager)
            return;

        if (zoomIn)
            WindowZoomIn(*mainWindow, atCursor);
        else
            WindowZoomOut(*mainWindow, atCursor);
    }
} // namespace OpenRCT2::Ui::Windows
