/*****************************************************************************
 * Copyright (c) 2014-2020 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 "../input/ShortcutManager.h"
#include "Window.h"

#include <openrct2-ui/interface/Widget.h>
#include <openrct2/drawing/Drawing.h>
#include <openrct2/localisation/Localisation.h>
#include <openrct2/sprites.h>

using namespace OpenRCT2;
using namespace OpenRCT2::Ui;

static constexpr const rct_string_id WINDOW_TITLE = STR_SHORTCUTS_TITLE;
static constexpr const int32_t WW = 420;
static constexpr const int32_t WH = 280;

static constexpr const int32_t WW_SC_MAX = 1200;
static constexpr const int32_t WH_SC_MAX = 800;

enum WINDOW_SHORTCUT_WIDGET_IDX
{
    WIDX_BACKGROUND,
    WIDX_TITLE,
    WIDX_CLOSE,
    WIDX_TAB_CONTENT_PANEL,
    WIDX_SCROLL,
    WIDX_RESET,
    WIDX_TAB_0,
};

// clang-format off
static rct_widget window_shortcut_widgets[] = {
    WINDOW_SHIM(WINDOW_TITLE, WW, WH),
    MakeWidget({0,    43}, {350, 287}, WindowWidgetType::Resize, WindowColour::Secondary),
    MakeWidget({4,    47}, {412, 245}, WindowWidgetType::Scroll, WindowColour::Primary, SCROLL_VERTICAL,           STR_SHORTCUT_LIST_TIP        ),
    MakeWidget({4, WH-15}, {150,  12}, WindowWidgetType::Button, WindowColour::Primary, STR_SHORTCUT_ACTION_RESET, STR_SHORTCUT_ACTION_RESET_TIP),
    WIDGETS_END,
};
// clang-format on

static constexpr const rct_string_id CHANGE_WINDOW_TITLE = STR_SHORTCUT_CHANGE_TITLE;
static constexpr const int32_t CHANGE_WW = 250;
static constexpr const int32_t CHANGE_WH = 80;

enum
{
    WIDX_REMOVE = 3
};

// clang-format off
static rct_widget window_shortcut_change_widgets[] = {
    WINDOW_SHIM(CHANGE_WINDOW_TITLE, CHANGE_WW, CHANGE_WH),
    MakeWidget({ 75, 56 }, { 100, 14 }, WindowWidgetType::Button, WindowColour::Primary, STR_SHORTCUT_REMOVE, STR_SHORTCUT_REMOVE_TIP),
    WIDGETS_END,
};
// clang-format on

class ChangeShortcutWindow final : public Window
{
private:
    std::string _shortcutId;
    rct_string_id _shortcutLocalisedName{};
    std::string _shortcutCustomName;

public:
    static ChangeShortcutWindow* Open(std::string_view shortcutId)
    {
        auto& shortcutManager = GetShortcutManager();
        auto registeredShortcut = shortcutManager.GetShortcut(shortcutId);
        if (registeredShortcut != nullptr)
        {
            window_close_by_class(WC_CHANGE_KEYBOARD_SHORTCUT);
            auto w = WindowCreate<ChangeShortcutWindow>(WC_CHANGE_KEYBOARD_SHORTCUT, CHANGE_WW, CHANGE_WH, WF_CENTRE_SCREEN);
            if (w != nullptr)
            {
                w->_shortcutId = shortcutId;
                w->_shortcutLocalisedName = registeredShortcut->LocalisedName;
                w->_shortcutCustomName = registeredShortcut->CustomName;
                shortcutManager.SetPendingShortcutChange(registeredShortcut->Id);
                return w;
            }
        }
        return nullptr;
    }

    void OnOpen() override
    {
        widgets = window_shortcut_change_widgets;
        enabled_widgets = (1ULL << WIDX_CLOSE) | (1ULL << WIDX_REMOVE);
        WindowInitScrollWidgets(this);
    }

    void OnClose() override
    {
        auto& shortcutManager = GetShortcutManager();
        shortcutManager.SetPendingShortcutChange({});
        NotifyShortcutKeysWindow();
    }

    void OnMouseUp(rct_widgetindex widgetIndex) override
    {
        switch (widgetIndex)
        {
            case WIDX_CLOSE:
                Close();
                break;
            case WIDX_REMOVE:
                Remove();
                break;
        }
    }

    void OnDraw(rct_drawpixelinfo& dpi) override
    {
        DrawWidgets(dpi);

        ScreenCoordsXY stringCoords(windowPos.x + 125, windowPos.y + 30);

        auto ft = Formatter();
        if (_shortcutCustomName.empty())
        {
            ft.Add<rct_string_id>(_shortcutLocalisedName);
        }
        else
        {
            ft.Add<rct_string_id>(STR_STRING);
            ft.Add<const char*>(_shortcutCustomName.c_str());
        }
        DrawTextWrapped(&dpi, stringCoords, 242, STR_SHORTCUT_CHANGE_PROMPT, ft, { TextAlignment::CENTRE });
    }

private:
    void NotifyShortcutKeysWindow();

    void Remove()
    {
        auto& shortcutManager = GetShortcutManager();
        auto* shortcut = shortcutManager.GetShortcut(_shortcutId);
        if (shortcut != nullptr)
        {
            shortcut->Current.clear();
            shortcutManager.SaveUserBindings();
        }
        Close();
    }
};

class ShortcutKeysWindow final : public Window
{
private:
    struct ShortcutStringPair
    {
        std::string ShortcutId;
        rct_string_id StringId = STR_NONE;
        std::string CustomString;
        std::string Binding;
    };

    struct ShortcutTabDesc
    {
        std::string_view IdGroup;
        uint32_t ImageId;
        uint32_t ImageDivisor;
        uint32_t ImageNumFrames;
    };

    std::vector<ShortcutTabDesc> _tabs;
    std::vector<rct_widget> _widgets;
    std::vector<ShortcutStringPair> _list;
    std::optional<size_t> _highlightedItem;
    size_t _currentTabIndex{};
    uint32_t _tabAnimationIndex{};

public:
    void OnOpen() override
    {
        InitialiseTabs();
        InitialiseWidgets();
        InitialiseList();

        min_width = WW;
        min_height = WH;
        max_width = WW_SC_MAX;
        max_height = WH_SC_MAX;
    }

    void OnResize() override
    {
        window_set_resize(this, min_width, min_height, max_width, max_height);
    }

    void OnUpdate() override
    {
        _tabAnimationIndex++;
        InvalidateWidget(static_cast<rct_widgetindex>(WIDX_TAB_0 + _currentTabIndex));
    }

    void OnMouseUp(rct_widgetindex widgetIndex) override
    {
        switch (widgetIndex)
        {
            case WIDX_CLOSE:
                Close();
                break;
            case WIDX_RESET:
                ResetAll();
                break;
            default:
            {
                auto tabIndex = static_cast<size_t>(widgetIndex - WIDX_TAB_0);
                if (tabIndex < _tabs.size())
                {
                    SetTab(tabIndex);
                }
            }
        }
    }

    void OnPrepareDraw() override
    {
        widgets[WIDX_BACKGROUND].right = width - 1;
        widgets[WIDX_BACKGROUND].bottom = height - 1;
        widgets[WIDX_TITLE].right = width - 2;
        widgets[WIDX_CLOSE].right = width - 3;
        widgets[WIDX_CLOSE].left = width - 13;
        widgets[WIDX_TAB_CONTENT_PANEL].right = width - 1;
        widgets[WIDX_TAB_CONTENT_PANEL].bottom = height - 1;
        widgets[WIDX_SCROLL].right = width - 5;
        widgets[WIDX_SCROLL].bottom = height - 19;
        widgets[WIDX_RESET].top = height - 16;
        widgets[WIDX_RESET].bottom = height - 5;
        window_align_tabs(this, WIDX_TAB_0, static_cast<rct_widgetindex>(WIDX_TAB_0 + _tabs.size()));

        // Set selected tab
        for (size_t i = 0; i < _tabs.size(); i++)
        {
            SetWidgetPressed(static_cast<rct_widgetindex>(WIDX_TAB_0 + i), false);
        }
        SetWidgetPressed(static_cast<rct_widgetindex>(WIDX_TAB_0 + _currentTabIndex), true);
    }

    void OnDraw(rct_drawpixelinfo& dpi) override
    {
        DrawWidgets(dpi);
        DrawTabImages(dpi);
    }

    ScreenSize OnScrollGetSize(int32_t scrollIndex) override
    {
        auto h = static_cast<int32_t>(_list.size() * SCROLLABLE_ROW_HEIGHT);
        auto bottom = std::max(0, h - widgets[WIDX_SCROLL].bottom + widgets[WIDX_SCROLL].top + 21);
        if (bottom < scrolls[0].v_top)
        {
            scrolls[0].v_top = bottom;
            Invalidate();
        }
        return { 0, h };
    }

    void OnScrollMouseOver(int32_t scrollIndex, const ScreenCoordsXY& screenCoords) override
    {
        auto index = static_cast<size_t>((screenCoords.y - 1) / SCROLLABLE_ROW_HEIGHT);
        if (index < _list.size())
        {
            _highlightedItem = index;
            Invalidate();
        }
    }

    void OnScrollMouseDown(int32_t scrollIndex, const ScreenCoordsXY& screenCoords) override
    {
        auto selectedItem = static_cast<size_t>((screenCoords.y - 1) / SCROLLABLE_ROW_HEIGHT);
        if (selectedItem < _list.size())
        {
            // Is this a separator?
            if (!_list[selectedItem].ShortcutId.empty())
            {
                auto& shortcut = _list[selectedItem];
                ChangeShortcutWindow::Open(shortcut.ShortcutId);
            }
        }
    }

    void OnScrollDraw(int32_t scrollIndex, rct_drawpixelinfo& dpi) override
    {
        auto dpiCoords = ScreenCoordsXY{ dpi.x, dpi.y };
        gfx_fill_rect(
            &dpi, { dpiCoords, dpiCoords + ScreenCoordsXY{ dpi.width - 1, dpi.height - 1 } }, ColourMapA[colours[1]].mid_light);

        // TODO: the line below is a workaround for what is presumably a bug with dpi->width
        //       see https://github.com/OpenRCT2/OpenRCT2/issues/11238 for details
        const auto scrollWidth = width - SCROLLBAR_WIDTH - 10;

        for (size_t i = 0; i < _list.size(); ++i)
        {
            auto y = static_cast<int32_t>(1 + i * SCROLLABLE_ROW_HEIGHT);
            if (y > dpi.y + dpi.height)
            {
                break;
            }

            if (y + SCROLLABLE_ROW_HEIGHT < dpi.y)
            {
                continue;
            }

            // Is this a separator?
            if (_list[i].ShortcutId.empty())
            {
                DrawSeparator(dpi, y, scrollWidth);
            }
            else
            {
                auto isHighlighted = _highlightedItem == i;
                DrawItem(dpi, y, scrollWidth, _list[i], isHighlighted);
            }
        }
    }

    void RefreshBindings()
    {
        InitialiseList();
    }

private:
    bool IsInCurrentTab(const RegisteredShortcut& shortcut)
    {
        auto groupFilter = _tabs[_currentTabIndex].IdGroup;
        auto group = shortcut.GetTopLevelGroup();
        if (groupFilter.empty())
        {
            // Check it doesn't belong in any other tab
            for (const auto& tab : _tabs)
            {
                if (!tab.IdGroup.empty())
                {
                    if (tab.IdGroup == group)
                    {
                        return false;
                    }
                }
            }
            return true;
        }

        return group == groupFilter;
    }

    void InitialiseList()
    {
        // Get shortcuts and sort by group
        auto shortcuts = GetShortcutsForCurrentTab();
        std::stable_sort(shortcuts.begin(), shortcuts.end(), [](const RegisteredShortcut* a, const RegisteredShortcut* b) {
            return a->GetGroup().compare(b->GetGroup()) < 0;
        });

        // Create list items with a separator between each group
        _list.clear();
        size_t index = 0;
        std::string group;
        for (const auto* shortcut : shortcuts)
        {
            if (group.empty())
            {
                group = shortcut->GetGroup();
            }
            else
            {
                auto groupName = shortcut->GetGroup();
                if (group != groupName)
                {
                    // Add separator
                    group = groupName;
                    _list.emplace_back();
                }
            }

            ShortcutStringPair ssp;
            ssp.ShortcutId = shortcut->Id;
            ssp.StringId = shortcut->LocalisedName;
            ssp.CustomString = shortcut->CustomName;
            ssp.Binding = shortcut->GetDisplayString();
            _list.push_back(std::move(ssp));
            index++;
        }

        Invalidate();
    }

    std::vector<const RegisteredShortcut*> GetShortcutsForCurrentTab()
    {
        std::vector<const RegisteredShortcut*> result;
        auto& shortcutManager = GetShortcutManager();
        for (const auto& shortcut : shortcutManager.Shortcuts)
        {
            if (IsInCurrentTab(shortcut.second))
            {
                result.push_back(&shortcut.second);
            }
        }
        return result;
    }

    void InitialiseTabs()
    {
        _tabs.clear();
        _tabs.push_back({ "interface", SPR_TAB_GEARS_0, 2, 4 });
        _tabs.push_back({ "view", SPR_G2_VIEW, 0, 0 });
        _tabs.push_back({ "window", SPR_TAB_PARK_ENTRANCE, 0, 0 });
        _tabs.push_back({ {}, SPR_TAB_WRENCH_0, 2, 16 });
    }

    void InitialiseWidgets()
    {
        enabled_widgets = (1ULL << WIDX_CLOSE) | (1ULL << WIDX_RESET);

        _widgets.clear();
        _widgets.insert(_widgets.begin(), std::begin(window_shortcut_widgets), std::end(window_shortcut_widgets) - 1);

        int32_t x = 3;
        for (size_t i = 0; i < _tabs.size(); i++)
        {
            auto tab = MakeTab({ x, 17 }, STR_NONE);
            _widgets.push_back(tab);
            x += 31;

            enabled_widgets |= (1ULL << (WIDX_TAB_0 + i));
        }

        _widgets.push_back(WIDGETS_END);
        widgets = _widgets.data();

        WindowInitScrollWidgets(this);
    }

    void SetTab(size_t index)
    {
        if (_currentTabIndex != index)
        {
            _currentTabIndex = index;
            _tabAnimationIndex = 0;
            InitialiseList();
        }
    }

    void ResetAll()
    {
        auto& shortcutManager = GetShortcutManager();
        for (const auto& item : _list)
        {
            auto shortcut = shortcutManager.GetShortcut(item.ShortcutId);
            if (shortcut != nullptr)
            {
                shortcut->Current = shortcut->Default;
            }
        }
        shortcutManager.SaveUserBindings();
        RefreshBindings();
    }

    void DrawTabImages(rct_drawpixelinfo& dpi) const
    {
        for (size_t i = 0; i < _tabs.size(); i++)
        {
            DrawTabImage(dpi, i);
        }
    }

    void DrawTabImage(rct_drawpixelinfo& dpi, size_t tabIndex) const
    {
        const auto& tabDesc = _tabs[tabIndex];
        auto widgetIndex = static_cast<rct_widgetindex>(WIDX_TAB_0 + tabIndex);
        if (!IsWidgetDisabled(widgetIndex))
        {
            auto imageId = tabDesc.ImageId;
            if (imageId != 0)
            {
                if (tabIndex == _currentTabIndex && tabDesc.ImageDivisor != 0 && tabDesc.ImageNumFrames != 0)
                {
                    auto frame = _tabAnimationIndex / tabDesc.ImageDivisor;
                    imageId += frame % tabDesc.ImageNumFrames;
                }

                const auto& widget = widgets[widgetIndex];
                gfx_draw_sprite(&dpi, ImageId(imageId), windowPos + ScreenCoordsXY{ widget.left, widget.top });
            }
        }
    }

    void DrawSeparator(rct_drawpixelinfo& dpi, int32_t y, int32_t scrollWidth)
    {
        const int32_t top = y + (SCROLLABLE_ROW_HEIGHT / 2) - 1;
        gfx_fill_rect(&dpi, { { 0, top }, { scrollWidth, top } }, ColourMapA[colours[0]].mid_dark);
        gfx_fill_rect(&dpi, { { 0, top + 1 }, { scrollWidth, top + 1 } }, ColourMapA[colours[0]].lightest);
    }

    void DrawItem(
        rct_drawpixelinfo& dpi, int32_t y, int32_t scrollWidth, const ShortcutStringPair& shortcut, bool isHighlighted)
    {
        auto format = STR_BLACK_STRING;
        if (isHighlighted)
        {
            format = STR_WINDOW_COLOUR_2_STRINGID;
            gfx_filter_rect(&dpi, { 0, y - 1, scrollWidth, y + (SCROLLABLE_ROW_HEIGHT - 2) }, FilterPaletteID::PaletteDarken1);
        }

        auto bindingOffset = (scrollWidth * 2) / 3;
        auto ft = Formatter();
        ft.Add<rct_string_id>(STR_SHORTCUT_ENTRY_FORMAT);
        if (shortcut.CustomString.empty())
        {
            ft.Add<rct_string_id>(shortcut.StringId);
        }
        else
        {
            ft.Add<rct_string_id>(STR_STRING);
            ft.Add<const char*>(shortcut.CustomString.c_str());
        }
        DrawTextEllipsised(&dpi, { 0, y - 1 }, bindingOffset, format, ft);

        if (!shortcut.Binding.empty())
        {
            ft = Formatter();
            ft.Add<rct_string_id>(STR_STRING);
            ft.Add<const char*>(shortcut.Binding.c_str());
            DrawTextEllipsised(&dpi, { bindingOffset, y - 1 }, 150, format, ft);
        }
    }
};

void ChangeShortcutWindow::NotifyShortcutKeysWindow()
{
    auto w = window_find_by_class(WC_KEYBOARD_SHORTCUT_LIST);
    if (w != nullptr)
    {
        static_cast<ShortcutKeysWindow*>(w)->RefreshBindings();
    }
}

rct_window* WindowShortcutKeysOpen()
{
    auto w = window_bring_to_front_by_class(WC_KEYBOARD_SHORTCUT_LIST);
    if (w == nullptr)
    {
        w = WindowCreate<ShortcutKeysWindow>(WC_KEYBOARD_SHORTCUT_LIST, WW, WH, WF_RESIZABLE);
    }
    return w;
}
