/*****************************************************************************
 * Copyright (c) 2014-2026 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 <SDL.h>
#include <SDL_keycode.h>
#include <iterator>
#include <openrct2-ui/interface/Widget.h>
#include <openrct2-ui/windows/Windows.h>
#include <openrct2/Context.h>
#include <openrct2/Input.h>
#include <openrct2/core/String.hpp>
#include <openrct2/core/UTF8.h>
#include <openrct2/drawing/ColourMap.h>
#include <openrct2/drawing/Drawing.String.h>
#include <openrct2/drawing/Drawing.h>
#include <openrct2/drawing/Rectangle.h>
#include <openrct2/drawing/Text.h>
#include <openrct2/localisation/Formatting.h>
#include <openrct2/localisation/StringIds.h>
#include <openrct2/ui/WindowManager.h>

using namespace OpenRCT2::Drawing;

namespace OpenRCT2::Ui::Windows
{
    static constexpr ScreenSize kWindowSize = { 250, 90 };

    enum WindowTextInputWidgetIdx
    {
        WIDX_BACKGROUND,
        WIDX_TITLE,
        WIDX_CLOSE,
        WIDX_CANCEL,
        WIDX_OKAY
    };

    static constexpr auto _textInputWidgets = makeWidgets(
        makeWindowShim(kStringIdNone, kWindowSize),
        makeWidget({ 170, 68 }, { 71, 14 }, WidgetType::button, WindowColour::secondary, STR_CANCEL),
        makeWidget({ 10, 68 }, { 71, 14 }, WidgetType::button, WindowColour::secondary, STR_OK));

    class TextInputWindow final : public Window
    {
    private:
        WidgetIdentifier _parentWidget{};

        std::string _title;
        StringId _titleStringId = kStringIdNone;

        std::string _description;
        StringId _descriptionStringId = kStringIdNone;
        Formatter _descriptionArgs;

        std::function<void(std::string_view)> _callback;
        std::function<void()> _cancelCallback;

        int32_t _cursorBlink{};
        size_t _maxInputLength{};
        u8string _buffer;

    public:
        void onOpen() override
        {
            setWidgets(_textInputWidgets);
            WindowInitScrollWidgets(*this);
            setParentWindow(nullptr, 0);
        }

        void setParentWindow(WindowBase* parentWindow, WidgetIndex widgetIndex)
        {
            // Save calling window details so that the information can be passed back to the correct window & widget
            if (parentWindow == nullptr)
            {
                _parentWidget.window.classification = WindowClass::null;
                _parentWidget.window.number = 0;
                _parentWidget.widgetIndex = 0;

                colours[0] = Drawing::Colour::grey;
                colours[1] = Drawing::Colour::grey;
                colours[2] = Drawing::Colour::grey;
            }
            else
            {
                _parentWidget.window.classification = parentWindow->classification;
                _parentWidget.window.number = parentWindow->number;
                _parentWidget.widgetIndex = widgetIndex;

                colours[0] = parentWindow->colours[1];
                colours[1] = parentWindow->colours[1];
                colours[2] = parentWindow->colours[1];
            }
        }

        void setTitle(StringId title, StringId description, const Formatter& decriptionArgs)
        {
            _titleStringId = title;
            _descriptionStringId = description;
            _descriptionArgs = decriptionArgs;
        }

        void setTitle(std::string_view title, std::string_view description)
        {
            _titleStringId = kStringIdNone;
            _title = title;
            _descriptionStringId = kStringIdNone;
            _description = description;
        }

        void setText(std::string_view text, size_t maxLength)
        {
            text = String::utf8TruncateCodePoints(text, maxLength);
            _buffer = u8string{ text };
            _maxInputLength = maxLength;
            SetTexboxSession(ContextStartTextInput(_buffer, maxLength));
        }

        void setCallback(std::function<void(std::string_view)> callback, std::function<void()> cancelCallback)
        {
            _callback = callback;
            _cancelCallback = cancelCallback;
        }

        void onClose() override
        {
            // Make sure that we take it out of the text input
            // mode otherwise problems may occur.
            ContextStopTextInput();
        }

        void onUpdate() override
        {
            if (HasParentWindow())
            {
                // If the calling window is closed then close the text input window
                auto parentWindow = GetParentWindow();
                if (parentWindow == nullptr)
                {
                    close();
                    return;
                }
            }

            // Used to blink the cursor.
            _cursorBlink++;
            if (_cursorBlink > 30)
                _cursorBlink = 0;

            invalidate();
        }

        void onMouseUp(WidgetIndex widgetIndex) override
        {
            switch (widgetIndex)
            {
                case WIDX_CANCEL:
                case WIDX_CLOSE:
                    ContextStopTextInput();
                    ExecuteCallback(false);
                    close();
                    break;
                case WIDX_OKAY:
                    ContextStopTextInput();
                    ExecuteCallback(true);
                    close();
            }
        }

        void onPrepareDraw() override
        {
            // Change window size if required.
            int32_t newHeight = CalculateWindowHeight(_buffer.data());
            if (newHeight != height)
            {
                WindowSetResize(*this, { kWindowSize.width, newHeight }, { kWindowSize.width, newHeight });
            }

            widgets[WIDX_OKAY].top = newHeight - 22;
            widgets[WIDX_OKAY].bottom = newHeight - 9;
            widgets[WIDX_CANCEL].top = newHeight - 22;
            widgets[WIDX_CANCEL].bottom = newHeight - 9;
            widgets[WIDX_BACKGROUND].bottom = newHeight - 1;

            // Set window title argument
            if (_titleStringId == kStringIdNone)
            {
                widgets[WIDX_TITLE].setString(_title.c_str());
            }
            else
            {
                widgets[WIDX_TITLE].setString(_titleStringId);
            }
        }

        void onDraw(RenderTarget& rt) override
        {
            drawWidgets(rt);

            auto screenCoords = windowPos + ScreenCoordsXY{ kWindowSize.width / 2, widgets[WIDX_TITLE].bottom + 13 };

            int32_t no_lines = 0;

            if (_descriptionStringId == kStringIdNone)
            {
                drawTextWrapped(rt, screenCoords, kWindowSize.width, _description, { colours[1], TextAlignment::centre });
            }
            else
            {
                drawTextWrapped(
                    rt, screenCoords, kWindowSize.width, _descriptionStringId, _descriptionArgs,
                    { colours[1], TextAlignment::centre });
            }

            screenCoords.y += 25;

            // String length needs to add 12 either side of box
            // +13 for cursor when max length.
            u8string wrappedString;
            wrapString(
                u8string_view{ _buffer.data(), _buffer.size() }, kWindowSize.width - (24 + 13), FontStyle::medium,
                &wrappedString, &no_lines);

            Rectangle::fillInset(
                rt,
                { { windowPos.x + 10, screenCoords.y },
                  { windowPos.x + kWindowSize.width - 10, screenCoords.y + 10 * (no_lines + 1) + 3 } },
                colours[1], Rectangle::BorderStyle::inset, Rectangle::FillBrightness::light,
                Rectangle::FillMode::dontLightenWhenInset);

            screenCoords.y += 1;

            const utf8* wrapPointer = wrappedString.data();
            size_t char_count = 0;
            uint8_t cur_drawn = 0;

            auto* textInput = GetTextboxSession();
            int32_t cursorX = 0;
            int32_t cursorY = 0;
            for (int32_t line = 0; line <= no_lines; line++)
            {
                screenCoords.x = windowPos.x + 12;
                drawText(
                    rt, screenCoords, wrapPointer,
                    { colours[1], FontStyle::medium, { TextPaintFlag::noFormatting }, TextAlignment::left });

                size_t string_length = GetStringSize(wrapPointer) - 1;
                if (!cur_drawn && (textInput->SelectionStart <= char_count + string_length))
                {
                    // Make a view of the string for measuring the width.
                    cursorX = windowPos.x + 13
                        + getStringWidth(
                                  u8string_view{ wrapPointer, textInput->SelectionStart - char_count }, FontStyle::medium,
                                  true);
                    cursorY = screenCoords.y;

                    int32_t textWidth = 6;
                    if (textInput->SelectionStart < strlen(_buffer.data()))
                    {
                        // Make a 1 utf8-character wide string for measuring the width
                        // of the currently selected character.
                        utf8 tmp[5] = {}; // This is easier than setting temp_string[0..5]
                        uint32_t codepoint = UTF8GetNext(_buffer.data() + textInput->SelectionStart, nullptr);
                        UTF8WriteCodepoint(tmp, codepoint);
                        textWidth = std::max(getStringWidth(tmp, FontStyle::medium, true) - 2, 4);
                    }

                    if (_cursorBlink > 15)
                    {
                        auto colour = getColourMap(colours[1].colour).midLight;
                        // TODO: palette index addition
                        Rectangle::fill(
                            rt, { { cursorX, screenCoords.y + 9 }, { cursorX + textWidth, screenCoords.y + 9 } },
                            static_cast<PaletteIndex>(EnumValue(colour) + 5));
                    }

                    cur_drawn++;
                }

                wrapPointer += string_length + 1;

                if (_buffer[char_count + string_length] == ' ')
                    char_count++;
                char_count += string_length;

                screenCoords.y += 10;
            }

            if (!cur_drawn)
            {
                cursorX = rt.lastStringPos.x;
                cursorY = screenCoords.y - 10;
            }

            // IME composition
            if (!String::isNullOrEmpty(textInput->ImeBuffer))
            {
                IMEComposition(cursorX, cursorY);
            }
        }

        void onReturnPressed()
        {
            ContextStopTextInput();
            ExecuteCallback(true);
            close();
        }

        int32_t CalculateWindowHeight(std::string_view text)
        {
            // String length needs to add 12 either side of box +13 for cursor when max length.
            int32_t numLines{};
            wrapString(text, kWindowSize.width - (24 + 13), FontStyle::medium, nullptr, &numLines);

            const auto textHeight = numLines * 10;
            return kWindowSize.height + textHeight + getTitleBarDiffNormal();
        }

    private:
        static void IMEComposition(int32_t cursorX, int32_t cursorY)
        {
            SDL_Rect rect = { cursorX, cursorY, 100, 100 };
            SDL_SetTextInputRect(&rect);
        }

        void ExecuteCallback(bool hasValue)
        {
            if (HasParentWindow())
            {
                if (hasValue)
                {
                    auto w = GetParentWindow();
                    if (w != nullptr)
                    {
                        w->onTextInput(_parentWidget.widgetIndex, _buffer);
                    }
                }
            }
            else
            {
                if (hasValue)
                {
                    if (_callback)
                    {
                        _callback(_buffer.data());
                    }
                }
                else
                {
                    if (_cancelCallback)
                    {
                        _cancelCallback();
                    }
                }
            }
        }

        bool HasParentWindow() const
        {
            return _parentWidget.window.classification != WindowClass::null;
        }

        WindowBase* GetParentWindow() const
        {
            auto* windowMgr = GetWindowManager();
            return HasParentWindow() ? windowMgr->FindByNumber(_parentWidget.window.classification, _parentWidget.window.number)
                                     : nullptr;
        }
    };

    void WindowTextInputRawOpen(
        WindowBase* call_w, WidgetIndex call_widget, StringId title, StringId description, const Formatter& descriptionArgs,
        const_utf8string existing_text, int32_t maxLength)
    {
        auto* windowMgr = GetWindowManager();
        windowMgr->CloseByClass(WindowClass::textinput);

        auto w = windowMgr->Create<TextInputWindow>(
            WindowClass::textinput, { kWindowSize.width, kWindowSize.height + 10 },
            { WindowFlag::centreScreen, WindowFlag::stickToFront });
        if (w != nullptr)
        {
            w->setParentWindow(call_w, call_widget);
            w->setTitle(title, description, descriptionArgs);
            w->setText(existing_text, maxLength);
        }
    }

    void WindowTextInputOpen(
        std::string_view title, std::string_view description, std::string_view initialValue, size_t maxLength,
        std::function<void(std::string_view)> callback, std::function<void()> cancelCallback)
    {
        auto* windowMgr = GetWindowManager();
        auto w = windowMgr->Create<TextInputWindow>(
            WindowClass::textinput, { kWindowSize.width, kWindowSize.height + 10 },
            { WindowFlag::centreScreen, WindowFlag::stickToFront });
        if (w != nullptr)
        {
            w->setTitle(title, description);
            w->setText(initialValue, maxLength);
            w->setCallback(callback, cancelCallback);
        }
    }

    void WindowTextInputOpen(
        WindowBase* call_w, WidgetIndex call_widget, StringId title, StringId description, const Formatter& descriptionArgs,
        StringId existing_text, uintptr_t existing_args, int32_t maxLength)
    {
        auto existingText = FormatStringIDLegacy(existing_text, &existing_args);
        WindowTextInputRawOpen(call_w, call_widget, title, description, descriptionArgs, existingText.c_str(), maxLength);
    }

    void WindowTextInputKey(WindowBase* w, uint32_t keycode)
    {
        const auto wndNumber = w->number;
        const auto wndClass = w->classification;

        // If the return button is pressed stop text input
        if (keycode == SDLK_RETURN || keycode == SDLK_KP_ENTER)
        {
            if (w->classification == WindowClass::textinput)
            {
                auto textInputWindow = static_cast<TextInputWindow*>(w);
                textInputWindow->onReturnPressed();
            }
        }

        // The window can be potentially closed within a callback, we need to check if its still alive.
        auto* windowMgr = GetWindowManager();
        w = windowMgr->FindByNumber(wndClass, wndNumber);
        if (w != nullptr)
            w->invalidate();
    }
} // namespace OpenRCT2::Ui::Windows
