/*****************************************************************************
 * 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 <mutex>
#include <openrct2-ui/interface/Widget.h>
#include <openrct2-ui/windows/Windows.h>
#include <openrct2/Diagnostic.h>
#include <openrct2/core/Console.hpp>
#include <openrct2/core/Http.h>
#include <openrct2/core/Json.hpp>
#include <openrct2/core/String.hpp>
#include <openrct2/drawing/Text.h>
#include <openrct2/localisation/Formatter.h>
#include <openrct2/localisation/Formatting.h>
#include <openrct2/localisation/StringIds.h>
#include <openrct2/object/ObjectList.h>
#include <openrct2/object/ObjectManager.h>
#include <openrct2/object/ObjectRepository.h>
#include <openrct2/platform/Platform.h>
#include <openrct2/ui/UiContext.h>
#include <openrct2/ui/WindowManager.h>
#include <openrct2/windows/Intent.h>
#include <sstream>
#include <string>
#include <vector>

namespace OpenRCT2::Ui::Windows
{
#ifndef DISABLE_HTTP

    class ObjectDownloader
    {
    private:
        static constexpr auto OPENRCT2_API_LEGACY_OBJECT_URL = "https://api.openrct2.io/objects/legacy/";

        struct DownloadStatusInfo
        {
            std::string Name;
            std::string Source;
            size_t Count{};
            size_t Total{};

            bool operator==(const DownloadStatusInfo& rhs) const
            {
                return Name == rhs.Name && Source == rhs.Source && Count == rhs.Count && Total == rhs.Total;
            }
            bool operator!=(const DownloadStatusInfo& rhs) const
            {
                return !(*this == rhs);
            }
        };

        std::vector<ObjectEntryDescriptor> _entries;
        std::vector<ObjectEntryDescriptor> _downloadedEntries;
        size_t _currentDownloadIndex{};
        std::mutex _downloadedEntriesMutex;
        std::mutex _queueMutex;
        bool _nextDownloadQueued{};

        DownloadStatusInfo _lastDownloadStatusInfo;
        DownloadStatusInfo _downloadStatusInfo;
        std::mutex _downloadStatusInfoMutex;
        std::string _lastDownloadSource;

        // TODO static due to INTENT_EXTRA_CALLBACK not allowing a std::function
        inline static bool _downloadingObjects;

    public:
        void Begin(const std::vector<ObjectEntryDescriptor>& entries)
        {
            _lastDownloadStatusInfo = {};
            _downloadStatusInfo = {};
            _lastDownloadSource = {};
            _entries = entries;
            _currentDownloadIndex = 0;
            _downloadingObjects = true;
            QueueNextDownload();
        }

        bool IsDownloading() const
        {
            return _downloadingObjects;
        }

        std::vector<ObjectEntryDescriptor> GetDownloadedEntries()
        {
            std::lock_guard<std::mutex> guard(_downloadedEntriesMutex);
            return _downloadedEntries;
        }

        void Update()
        {
            std::lock_guard guard(_queueMutex);
            if (_nextDownloadQueued)
            {
                _nextDownloadQueued = false;
                NextDownload();
            }
            UpdateStatusBox();
        }

    private:
        void UpdateStatusBox()
        {
            std::lock_guard<std::mutex> guard(_downloadStatusInfoMutex);
            if (_lastDownloadStatusInfo != _downloadStatusInfo)
            {
                _lastDownloadStatusInfo = _downloadStatusInfo;

                if (_downloadStatusInfo == DownloadStatusInfo())
                {
                    ContextForceCloseWindowByClass(WindowClass::NetworkStatus);
                }
                else
                {
                    char str_downloading_objects[256]{};
                    Formatter ft;
                    if (_downloadStatusInfo.Source.empty())
                    {
                        ft.Add<int16_t>(static_cast<int16_t>(_downloadStatusInfo.Count));
                        ft.Add<int16_t>(static_cast<int16_t>(_downloadStatusInfo.Total));
                        ft.Add<char*>(_downloadStatusInfo.Name.c_str());
                        OpenRCT2::FormatStringLegacy(
                            str_downloading_objects, sizeof(str_downloading_objects), STR_DOWNLOADING_OBJECTS, ft.Data());
                    }
                    else
                    {
                        ft.Add<char*>(_downloadStatusInfo.Name.c_str());
                        ft.Add<char*>(_downloadStatusInfo.Source.c_str());
                        ft.Add<int16_t>(static_cast<int16_t>(_downloadStatusInfo.Count));
                        ft.Add<int16_t>(static_cast<int16_t>(_downloadStatusInfo.Total));
                        OpenRCT2::FormatStringLegacy(
                            str_downloading_objects, sizeof(str_downloading_objects), STR_DOWNLOADING_OBJECTS_FROM, ft.Data());
                    }

                    auto intent = Intent(WindowClass::NetworkStatus);
                    intent.PutExtra(INTENT_EXTRA_MESSAGE, std::string(str_downloading_objects));
                    intent.PutExtra(INTENT_EXTRA_CALLBACK, []() -> void { _downloadingObjects = false; });
                    ContextOpenIntent(&intent);
                }
            }
        }

        void UpdateProgress(const DownloadStatusInfo& info)
        {
            std::lock_guard<std::mutex> guard(_downloadStatusInfoMutex);
            _downloadStatusInfo = info;
        }

        void QueueNextDownload()
        {
            std::lock_guard guard(_queueMutex);
            _nextDownloadQueued = true;
        }

        void DownloadObject(const ObjectEntryDescriptor& entry, const std::string& name, const std::string& url)
        {
            try
            {
                Console::WriteLine("Downloading %s", url.c_str());
                Http::Request req;
                req.method = Http::Method::GET;
                req.url = url;
                Http::DoAsync(req, [this, entry, name](Http::Response response) {
                    if (response.status == Http::Status::Ok)
                    {
                        // Check that download operation hasn't been cancelled
                        if (_downloadingObjects)
                        {
                            auto data = reinterpret_cast<uint8_t*>(response.body.data());
                            auto dataLen = response.body.size();

                            auto& objRepo = OpenRCT2::GetContext()->GetObjectRepository();
                            objRepo.AddObjectFromFile(ObjectGeneration::DAT, name, data, dataLen);

                            std::lock_guard<std::mutex> guard(_downloadedEntriesMutex);
                            _downloadedEntries.push_back(entry);
                        }
                    }
                    else
                    {
                        Console::Error::WriteLine("  Failed to download %s", name.c_str());
                    }
                    QueueNextDownload();
                });
            }
            catch (const std::exception&)
            {
                Console::Error::WriteLine("  Failed to download %s", name.c_str());
                QueueNextDownload();
            }
        }

        void NextDownload()
        {
            if (!_downloadingObjects || _currentDownloadIndex >= _entries.size())
            {
                // Finished...
                _downloadingObjects = false;
                UpdateProgress({});
                return;
            }

            auto& entry = _entries[_currentDownloadIndex];
            auto name = String::trim(std::string(entry.GetName()));
            LOG_VERBOSE("Downloading object: [%s]:", name.c_str());
            _currentDownloadIndex++;
            UpdateProgress({ name, _lastDownloadSource, _currentDownloadIndex, _entries.size() });
            try
            {
                Http::Request req;
                req.method = Http::Method::GET;
                req.url = OPENRCT2_API_LEGACY_OBJECT_URL + name;
                Http::DoAsync(req, [this, entry, name](Http::Response response) {
                    if (response.status == Http::Status::Ok)
                    {
                        auto jresponse = Json::FromString(response.body);
                        if (jresponse.is_object())
                        {
                            auto objName = Json::GetString(jresponse["name"]);
                            auto source = Json::GetString(jresponse["source"]);
                            auto downloadLink = Json::GetString(jresponse["download"]);
                            if (!downloadLink.empty())
                            {
                                _lastDownloadSource = source;
                                UpdateProgress({ name, source, _currentDownloadIndex, _entries.size() });
                                DownloadObject(entry, objName, downloadLink);
                            }
                        }
                    }
                    else if (response.status == Http::Status::NotFound)
                    {
                        Console::Error::WriteLine("  %s not found", name.c_str());
                        QueueNextDownload();
                    }
                    else
                    {
                        Console::Error::WriteLine(
                            "  %s query failed (status %d)", name.c_str(), static_cast<int32_t>(response.status));
                        QueueNextDownload();
                    }
                });
            }
            catch (const std::exception&)
            {
                Console::Error::WriteLine("  Failed to query %s", name.c_str());
            }
        }
    };

#endif

    enum WindowObjectLoadErrorWidgetIdx
    {
        WIDX_BACKGROUND,
        WIDX_TITLE,
        WIDX_CLOSE,
        WIDX_COLUMN_OBJECT_NAME,
        WIDX_COLUMN_OBJECT_SOURCE,
        WIDX_COLUMN_OBJECT_TYPE,
        WIDX_SCROLL,
        WIDX_COPY_CURRENT,
        WIDX_COPY_ALL,
        WIDX_DOWNLOAD_ALL
    };

    static constexpr StringId WINDOW_TITLE = STR_OBJECT_LOAD_ERROR_TITLE;
    static constexpr int32_t WW = 450;
    static constexpr int32_t WH = 400;
    static constexpr int32_t WW_LESS_PADDING = WW - 5;
    constexpr int32_t NAME_COL_LEFT = 4;
    constexpr int32_t SOURCE_COL_LEFT = (WW_LESS_PADDING / 4) + 1;
    constexpr int32_t TYPE_COL_LEFT = 5 * WW_LESS_PADDING / 8 + 1;

    // clang-format off
    static constexpr Widget window_object_load_error_widgets[] = {
        WINDOW_SHIM(WINDOW_TITLE, WW, WH),
        MakeWidget({  NAME_COL_LEFT,  57}, {108,  14}, WindowWidgetType::TableHeader, WindowColour::Primary, STR_OBJECT_NAME                         ), // 'Object name' header
        MakeWidget({SOURCE_COL_LEFT,  57}, {166,  14}, WindowWidgetType::TableHeader, WindowColour::Primary, STR_OBJECT_SOURCE                       ), // 'Object source' header
        MakeWidget({  TYPE_COL_LEFT,  57}, {166,  14}, WindowWidgetType::TableHeader, WindowColour::Primary, STR_OBJECT_TYPE                         ), // 'Object type' header
        MakeWidget({  NAME_COL_LEFT,  70}, {442, 298}, WindowWidgetType::Scroll,      WindowColour::Primary, SCROLL_VERTICAL                         ), // Scrollable list area
        MakeWidget({  NAME_COL_LEFT, 377}, {145,  14}, WindowWidgetType::Button,      WindowColour::Primary, STR_COPY_SELECTED, STR_COPY_SELECTED_TIP), // Copy selected button
        MakeWidget({            152, 377}, {145,  14}, WindowWidgetType::Button,      WindowColour::Primary, STR_COPY_ALL,      STR_COPY_ALL_TIP     ), // Copy all button
    #ifndef DISABLE_HTTP
        MakeWidget({            300, 377}, {146,  14}, WindowWidgetType::Button,      WindowColour::Primary, STR_DOWNLOAD_ALL,  STR_DOWNLOAD_ALL_TIP ), // Download all button
    #endif
    };
    // clang-format on

    /**
     *  Returns an StringId that represents an RCTObjectEntry's type.
     *
     *  Could possibly be moved out of the window file if other
     *  uses exist and a suitable location is found.
     */
    static constexpr StringId GetStringFromObjectType(const ObjectType type)
    {
        switch (type)
        {
            case ObjectType::ride:
                return STR_OBJECT_SELECTION_RIDE_VEHICLES_ATTRACTIONS;
            case ObjectType::smallScenery:
                return STR_OBJECT_SELECTION_SMALL_SCENERY;
            case ObjectType::largeScenery:
                return STR_OBJECT_SELECTION_LARGE_SCENERY;
            case ObjectType::walls:
                return STR_OBJECT_SELECTION_WALLS_FENCES;
            case ObjectType::banners:
                return STR_OBJECT_SELECTION_PATH_SIGNS;
            case ObjectType::paths:
                return STR_OBJECT_SELECTION_FOOTPATHS;
            case ObjectType::pathAdditions:
                return STR_OBJECT_SELECTION_PATH_EXTRAS;
            case ObjectType::sceneryGroup:
                return STR_OBJECT_SELECTION_SCENERY_GROUPS;
            case ObjectType::parkEntrance:
                return STR_OBJECT_SELECTION_PARK_ENTRANCE;
            case ObjectType::water:
                return STR_OBJECT_SELECTION_WATER;
            default:
                return STR_UNKNOWN_OBJECT_TYPE;
        }
    }

    class ObjectLoadErrorWindow final : public Window
    {
    private:
        std::vector<ObjectEntryDescriptor> _invalidEntries;
        int32_t _highlightedIndex = -1;
        std::string _filePath;
#ifndef DISABLE_HTTP
        ObjectDownloader _objDownloader;
        bool _updatedListAfterDownload{};

        void DownloadAllObjects()
        {
            if (!_objDownloader.IsDownloading())
            {
                _updatedListAfterDownload = false;
                _objDownloader.Begin(_invalidEntries);
            }
        }

        void UpdateObjectList()
        {
            const auto entries = _objDownloader.GetDownloadedEntries();
            for (auto& de : entries)
            {
                _invalidEntries.erase(
                    std::remove_if(
                        _invalidEntries.begin(), _invalidEntries.end(),
                        [de](const ObjectEntryDescriptor& e) { return de.GetName() == e.GetName(); }),
                    _invalidEntries.end());
            }
            no_list_items = static_cast<uint16_t>(_invalidEntries.size());
        }
#endif

        /**
         *  Returns a newline-separated string listing all object names.
         *  Used for placing all names on the clipboard.
         */
        void CopyObjectNamesToClipboard()
        {
            std::stringstream stream;
            for (uint16_t i = 0; i < no_list_items; i++)
            {
                const auto& entry = _invalidEntries[i];
                stream << entry.GetName();
                stream << PLATFORM_NEWLINE;
            }

            const auto clip = stream.str();
            OpenRCT2::GetContext()->GetUiContext()->SetClipboardText(clip.c_str());
        }

        void SelectObjectFromList(const int32_t index)
        {
            if (index < 0 || index > no_list_items)
            {
                selected_list_item = -1;
            }
            else
            {
                selected_list_item = index;
            }
            InvalidateWidget(WIDX_SCROLL);
        }

    public:
        void OnOpen() override
        {
            SetWidgets(window_object_load_error_widgets);

            WindowInitScrollWidgets(*this);
            colours[0] = COLOUR_LIGHT_BLUE;
            colours[1] = COLOUR_LIGHT_BLUE;
            colours[2] = COLOUR_LIGHT_BLUE;
        }

        void OnClose() override
        {
            _invalidEntries.clear();
            _invalidEntries.shrink_to_fit();
        }

        void OnMouseUp(const WidgetIndex widgetIndex) override
        {
            switch (widgetIndex)
            {
                case WIDX_CLOSE:
                    Close();
                    return;
                case WIDX_COPY_CURRENT:
                    if (selected_list_item > -1 && selected_list_item < no_list_items)
                    {
                        const auto name = std::string(_invalidEntries[selected_list_item].GetName());
                        OpenRCT2::GetContext()->GetUiContext()->SetClipboardText(name.c_str());
                    }
                    break;
                case WIDX_COPY_ALL:
                    CopyObjectNamesToClipboard();
                    break;
#ifndef DISABLE_HTTP
                case WIDX_DOWNLOAD_ALL:
                    DownloadAllObjects();
                    break;
#endif
            }
        }

        void OnUpdate() override
        {
            frame_no++;

            // Check if the mouse is hovering over the list
            if (!WidgetIsHighlighted(*this, WIDX_SCROLL))
            {
                _highlightedIndex = -1;
                InvalidateWidget(WIDX_SCROLL);
            }

#ifndef DISABLE_HTTP
            _objDownloader.Update();

            // Remove downloaded objects from our invalid entry list
            if (_objDownloader.IsDownloading())
            {
                // Don't do this too often as it isn't particularly efficient
                if (frame_no % 64 == 0)
                {
                    UpdateObjectList();
                }
            }
            else if (!_updatedListAfterDownload)
            {
                UpdateObjectList();
                _updatedListAfterDownload = true;
            }
#endif
        }

        ScreenSize OnScrollGetSize(const int32_t scrollIndex) override
        {
            return ScreenSize(0, no_list_items * kScrollableRowHeight);
        }

        void OnScrollMouseDown(const int32_t scrollIndex, const ScreenCoordsXY& screenCoords) override
        {
            const auto selectedItem = screenCoords.y / kScrollableRowHeight;
            SelectObjectFromList(selectedItem);
        }

        void OnScrollMouseOver(const int32_t scrollIndex, const ScreenCoordsXY& screenCoords) override
        {
            // Highlight item that the cursor is over, or remove highlighting if none
            const auto selectedItem = screenCoords.y / kScrollableRowHeight;
            if (selectedItem < 0 || selectedItem >= no_list_items)
                _highlightedIndex = -1;
            else
                _highlightedIndex = selectedItem;

            InvalidateWidget(WIDX_SCROLL);
        }

        void OnDraw(DrawPixelInfo& dpi) override
        {
            WindowDrawWidgets(*this, dpi);

            // Draw explanatory message
            auto ft = Formatter();
            ft.Add<StringId>(STR_OBJECT_ERROR_WINDOW_EXPLANATION);
            DrawTextWrapped(dpi, windowPos + ScreenCoordsXY{ 5, 18 }, WW - 10, STR_BLACK_STRING, ft);

            // Draw file name
            ft = Formatter();
            ft.Add<StringId>(STR_OBJECT_ERROR_WINDOW_FILE);
            ft.Add<utf8*>(_filePath.c_str());
            DrawTextEllipsised(dpi, { windowPos.x + 5, windowPos.y + 43 }, WW - 5, STR_BLACK_STRING, ft);
        }

        void OnScrollDraw(const int32_t scrollIndex, DrawPixelInfo& dpi) override
        {
            auto dpiCoords = ScreenCoordsXY{ dpi.x, dpi.y };
            GfxFillRect(
                dpi, { dpiCoords, dpiCoords + ScreenCoordsXY{ dpi.width - 1, dpi.height - 1 } },
                ColourMapA[colours[1].colour].mid_light);
            const int32_t listWidth = widgets[WIDX_SCROLL].width();

            for (int32_t i = 0; i < no_list_items; i++)
            {
                ScreenCoordsXY screenCoords;
                screenCoords.y = i * kScrollableRowHeight;
                if (screenCoords.y > dpi.y + dpi.height)
                    break;

                if (screenCoords.y + kScrollableRowHeight < dpi.y)
                    continue;

                const auto screenRect = ScreenRect{ { 0, screenCoords.y },
                                                    { listWidth, screenCoords.y + kScrollableRowHeight - 1 } };
                // If hovering over item, change the color and fill the backdrop.
                if (i == selected_list_item)
                    GfxFillRect(dpi, screenRect, ColourMapA[colours[1].colour].darker);
                else if (i == _highlightedIndex)
                    GfxFillRect(dpi, screenRect, ColourMapA[colours[1].colour].mid_dark);
                else if ((i & 1) != 0) // odd / even check
                    GfxFillRect(dpi, screenRect, ColourMapA[colours[1].colour].light);

                // Draw the actual object entry's name...
                screenCoords.x = NAME_COL_LEFT - 3;

                const auto& entry = _invalidEntries[i];

                auto name = entry.GetName();
                char buffer[256];
                String::set(buffer, sizeof(buffer), name.data(), name.size());
                DrawText(dpi, screenCoords, { COLOUR_DARK_GREEN }, buffer);

                if (entry.Generation == ObjectGeneration::DAT)
                {
                    // ... source game ...
                    const auto sourceStringId = ObjectManagerGetSourceGameString(entry.Entry.GetSourceGame());
                    DrawTextBasic(dpi, { SOURCE_COL_LEFT - 3, screenCoords.y }, sourceStringId, {}, { COLOUR_DARK_GREEN });
                }

                // ... and type
                const auto type = GetStringFromObjectType(entry.GetType());
                DrawTextBasic(dpi, { TYPE_COL_LEFT - 3, screenCoords.y }, type, {}, { COLOUR_DARK_GREEN });
            }
        }

        void OnResize() override
        {
            ResizeFrame();
        }

        void Initialise(utf8* path, const size_t numMissingObjects, const ObjectEntryDescriptor* missingObjects)
        {
            _invalidEntries = std::vector<ObjectEntryDescriptor>(missingObjects, missingObjects + numMissingObjects);

            // Refresh list items and path
            no_list_items = static_cast<uint16_t>(numMissingObjects);
            _filePath = path;

            Invalidate();
        }
    };

    WindowBase* ObjectLoadErrorOpen(utf8* path, size_t numMissingObjects, const ObjectEntryDescriptor* missingObjects)
    {
        // Check if window is already open
        auto* windowMgr = Ui::GetWindowManager();
        auto* window = windowMgr->BringToFrontByClass(WindowClass::ObjectLoadError);
        if (window == nullptr)
        {
            window = windowMgr->Create<ObjectLoadErrorWindow>(WindowClass::ObjectLoadError, WW, WH, 0);
        }

        static_cast<ObjectLoadErrorWindow*>(window)->Initialise(path, numMissingObjects, missingObjects);

        return window;
    }
} // namespace OpenRCT2::Ui::Windows
