/*
 * Fooyin
 * Copyright © 2023, Luke Taylor <luket@pm.me>
 *
 * Fooyin is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Fooyin is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Fooyin.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

#include "searchwidget.h"

#include "internalguisettings.h"
#include "playlist/playlistcontroller.h"
#include "playlist/playlistuicontroller.h"
#include "playlist/playlistwidget.h"
#include "searchcontroller.h"

#include <core/coresettings.h>
#include <core/library/musiclibrary.h>
#include <core/playlist/playlisthandler.h>
#include <core/scripting/trackqueryfilter.h>
#include <gui/guiconstants.h>
#include <gui/guisettings.h>
#include <gui/iconloader.h>
#include <gui/widgets/popuplineedit.h>
#include <utils/actions/actioncontainer.h>
#include <utils/async.h>
#include <utils/settings/settingsmanager.h>
#include <utils/utils.h>

#include <QDesktopServices>
#include <QHBoxLayout>
#include <QJsonObject>
#include <QKeyEvent>
#include <QLineEdit>
#include <QMenu>
#include <QStyleOptionFrame>

using namespace Qt::StringLiterals;

constexpr auto QuickSearchState = "Searching/QuickSearchState";

namespace Fooyin {
SearchWidget::SearchWidget(SearchController* controller, PlaylistController* playlistController, MusicLibrary* library,
                           SettingsManager* settings, QWidget* parent)
    : FyWidget{parent}
    , m_searchController{controller}
    , m_playlistController{playlistController}
    , m_playlistHandler{m_playlistController->playlistHandler()}
    , m_library{library}
    , m_settings{settings}
    , m_searchBox{new QLineEdit(this)}
    , m_defaultPlaceholder{tr("Search library…")}
    , m_mode{SearchMode::Library}
    , m_searchRequestToken{0}
    , m_forceNewPlaylist{false}
    , m_unconnected{true}
    , m_exclusivePlaylist{false}
    , m_autoSearch{!isQuickSearch()}
    , m_showAll{false}
{
    setObjectName(SearchWidget::name());

    auto* layout = new QHBoxLayout(this);
    layout->setContentsMargins({});
    layout->addWidget(m_searchBox);

    m_searchBox->setPlaceholderText(m_defaultPlaceholder);
    m_searchBox->setClearButtonEnabled(true);

    m_searchController->registerSetFunction(id(), [this](const QString& search) { m_searchBox->setText(search); });

    setFocusProxy(m_searchBox);
    loadColours();

    QObject::connect(m_searchBox, &QLineEdit::textChanged, this, [this]() {
        if(m_autoSearch) {
            m_searchTimer.start((m_settings->value<Settings::Gui::SearchAutoDelay>() + 1) * 30, this);
        }
    });
    QObject::connect(m_searchController, &SearchController::connectionChanged, this, [this](const Id& widgetId) {
        if(widgetId == id()) {
            updateConnectedState();
        }
    });

    auto* selectReceiver = new QAction(tr("Options"), this);
    Gui::setThemeIcon(selectReceiver, Constants::Icons::Options);
    QObject::connect(selectReceiver, &QAction::triggered, this, [this]() { showOptionsMenu(); });
    m_searchBox->addAction(selectReceiver, QLineEdit::TrailingPosition);

    m_settings->subscribe<Settings::Gui::IconTheme>(this,
                                                    [selectReceiver]() { Gui::refreshThemeIcon(selectReceiver); });
    m_settings->subscribe<Settings::Gui::SearchErrorBg>(this, &SearchWidget::loadColours);
    m_settings->subscribe<Settings::Gui::SearchErrorFg>(this, &SearchWidget::loadColours);
}

SearchWidget::~SearchWidget()
{
    m_searchController->removeConnectedWidgets(id());
}

QString SearchWidget::defaultPlaylistName()
{
    return tr("Search Results");
}

QString SearchWidget::name() const
{
    return tr("Search Bar");
}

QString SearchWidget::layoutName() const
{
    return u"SearchBar"_s;
}

void SearchWidget::layoutEditingMenu(QMenu* menu)
{
    auto* manageConnections = new QAction(tr("Manage connected widgets"), this);
    QObject::connect(manageConnections, &QAction::triggered, this,
                     [this]() { m_searchController->setupWidgetConnections(id()); });
    menu->addAction(manageConnections);
}

void SearchWidget::saveLayoutData(QJsonObject& layout)
{
    layout["AutoSearch"_L1] = m_autoSearch;
    layout["ShowAll"_L1]    = m_showAll;
    layout["SearchMode"_L1] = static_cast<quint8>(m_mode);

    const QString placeholderText = m_searchBox->placeholderText();
    if(!placeholderText.isEmpty() && placeholderText != m_defaultPlaceholder) {
        layout["Placeholder"_L1] = placeholderText;
    }

    const auto connectedWidgets = m_searchController->connectedWidgetIds(id());

    if(connectedWidgets.empty()) {
        return;
    }

    QStringList widgetIds;
    std::ranges::transform(connectedWidgets, std::back_inserter(widgetIds), [](const Id& id) { return id.name(); });

    layout["Widgets"_L1] = widgetIds.join(u"|"_s);
}

void SearchWidget::loadLayoutData(const QJsonObject& layout)
{
    if(layout.contains("AutoSearch"_L1)) {
        m_autoSearch = layout.value("AutoSearch"_L1).toBool();
    }

    if(layout.contains("ShowAll"_L1)) {
        m_showAll = layout.value("ShowAll"_L1).toBool();
    }

    if(layout.contains("SearchMode"_L1)) {
        m_mode = static_cast<SearchMode>(layout.value("SearchMode"_L1).toVariant().value<quint8>());
    }

    if(layout.contains("Placeholder"_L1)) {
        m_searchBox->setPlaceholderText(layout.value("Placeholder"_L1).toString());
    }

    if(!layout.contains("Widgets"_L1)) {
        return;
    }

    const QStringList widgetIds = layout.value("Widgets"_L1).toString().split(u'|');

    if(widgetIds.isEmpty()) {
        return;
    }

    IdSet connectedWidgets;

    for(const QString& id : widgetIds) {
        connectedWidgets.emplace(id.trimmed());
    }

    m_searchController->setConnectedWidgets(id(), connectedWidgets);
}

QSize SearchWidget::sizeHint() const
{
    QSize size = m_searchBox->sizeHint();
    size.rwidth() += 400;
    return size;
}

QSize SearchWidget::minimumSizeHint() const
{
    return m_searchBox->minimumSizeHint();
}

void SearchWidget::showEvent(QShowEvent* event)
{
    updateConnectedState();

    if(isQuickSearch()) {
        const FyStateSettings stateSettings;
        const QJsonObject layoutData = stateSettings.value(QuickSearchState).toJsonObject();
        loadLayoutData(layoutData);
        if(m_showAll && m_searchBox->text().isEmpty()) {
            searchChanged();
        }
    }

    FyWidget::showEvent(event);
}

void SearchWidget::closeEvent(QCloseEvent* event)
{
    if(isQuickSearch()) {
        QJsonObject layoutData;
        saveLayoutData(layoutData);

        FyStateSettings stateSettings;
        stateSettings.setValue(QuickSearchState, layoutData);
    }
    FyWidget::closeEvent(event);
}

void SearchWidget::keyPressEvent(QKeyEvent* event)
{
    const int key        = event->key();
    const auto modifiers = event->modifiers();

    if(key == Qt::Key_Enter || key == Qt::Key_Return) {
        if(modifiers == Qt::ControlModifier) {
            m_forceNewPlaylist = true;
        }
        else if(modifiers == Qt::AltModifier) {
            m_forceMode = SearchMode::Playlist;
        }
        else if(modifiers == Qt::ShiftModifier) {
            m_forceMode = SearchMode::AllPlaylists;
        }
        else if(modifiers == (Qt::ControlModifier | Qt::AltModifier)) {
            m_forceMode        = SearchMode::Playlist;
            m_forceNewPlaylist = true;
        }
        else if(modifiers == (Qt::ControlModifier | Qt::AltModifier | Qt::ShiftModifier)) {
            m_forceMode        = SearchMode::AllPlaylists;
            m_forceNewPlaylist = true;
        }

        searchChanged(true);
    }
    else if(key == Qt::Key_Escape) {
        if(isQuickSearch()) {
            close();
        }
        else if(!m_searchBox->text().isEmpty()) {
            m_searchBox->clear();
        }
        event->accept();
        return;
    }
    else if(key == Qt::Key_Backspace && modifiers == Qt::ControlModifier) {
        deleteWord();
    }

    FyWidget::keyPressEvent(event);
}

void SearchWidget::timerEvent(QTimerEvent* event)
{
    if(event->timerId() == m_searchTimer.timerId()) {
        m_searchTimer.stop();
        searchChanged();
    }
    FyWidget::timerEvent(event);
}

bool SearchWidget::isQuickSearch() const
{
    return parentWidget() == nullptr;
}

Playlist* SearchWidget::findOrAddPlaylist(const TrackList& tracks)
{
    QString searchResultsName = m_settings->value<Settings::Gui::SearchPlaylistName>();
    const QString searchText  = m_searchBox->text();
    const bool appendSearch   = m_settings->value<Settings::Gui::SearchPlaylistAppendSearch>() && !searchText.isEmpty();

    if(!m_forceNewPlaylist) {
        const auto playlists = m_playlistHandler->playlists();
        for(auto* playlist : playlists) {
            if(playlist->name().startsWith(searchResultsName)) {
                if(appendSearch) {
                    searchResultsName.append(u" [%1]"_s.arg(searchText));
                }
                if(searchResultsName != playlist->name()) {
                    m_playlistHandler->renamePlaylist(playlist->id(), searchResultsName);
                }
                m_playlistHandler->replacePlaylistTracks(playlist->id(), tracks);
                return playlist;
            }
        }
    }

    const bool forceNew = std::exchange(m_forceNewPlaylist, false);

    if(auto* playlist = forceNew ? m_playlistHandler->createNewPlaylist(searchResultsName, tracks)
                                 : m_playlistHandler->createPlaylist(searchResultsName, tracks)) {
        if(appendSearch) {
            searchResultsName.append(u" (%1)"_s.arg(searchText));
            m_playlistHandler->renamePlaylist(playlist->id(), searchResultsName);
        }
        m_playlistController->changeCurrentPlaylist(playlist);
        return playlist;
    }

    return nullptr;
}

PlaylistTrackList SearchWidget::getTracksToSearch(SearchMode mode) const
{
    switch(mode) {
        case SearchMode::AllPlaylists: {
            const QString searchResultsName = m_settings->value<Settings::Gui::SearchPlaylistName>();
            const auto playlists            = m_playlistHandler->playlists();
            PlaylistTrackList allTracks;
            for(const Playlist* playlist : playlists) {
                if(!playlist->name().contains(searchResultsName)) {
                    const PlaylistTrackList playlistTracks = playlist->playlistTracks();
                    allTracks.insert(allTracks.end(), playlistTracks.cbegin(), playlistTracks.cend());
                }
            }
            return allTracks;
        }
        case SearchMode::Library:
            return PlaylistTrack::fromTracks(m_library->tracks(), {});
        case SearchMode::PlaylistFilter:
            return {};
        case SearchMode::Playlist:
            if(m_playlistController->currentPlaylist()) {
                return m_playlistController->currentPlaylist()->playlistTracks();
            }
            [[fallthrough]];
        default:
            return {};
    }
}

void SearchWidget::deleteWord()
{
    QString text             = m_searchBox->text();
    const int cursorPosition = m_searchBox->cursorPosition();

    static const QRegularExpression wordRegex{u"\\W"_s};

    int lastWordBoundary = static_cast<int>(text.lastIndexOf(wordRegex, cursorPosition - 1));
    if(lastWordBoundary == -1) {
        lastWordBoundary = 0;
    }

    m_searchBox->setText(text.remove(lastWordBoundary, cursorPosition - lastWordBoundary));
    m_searchBox->setCursorPosition(lastWordBoundary);
}

bool SearchWidget::handleFilteredTracks(SearchMode mode, const PlaylistTrackList& tracks)
{
    if(tracks.empty()) {
        if(!m_exclusivePlaylist && mode != SearchMode::PlaylistFilter && !m_searchBox->text().isEmpty()) {
            handleSearchFailed();
            return false;
        }
    }

    resetColours();

    if(m_exclusivePlaylist || mode == SearchMode::PlaylistFilter) {
        m_searchController->changeSearch(id(), currentSearchRequest());
    }
    else {
        if(auto* playlist = findOrAddPlaylist(PlaylistTrack::toTracks(tracks))) {
            m_playlistController->changeCurrentPlaylist(playlist);
        }
    }

    if(!m_autoSearch && m_settings->value<Settings::Gui::SearchSuccessClear>()) {
        m_searchBox->clear();
    }
    if(!m_autoSearch && m_settings->value<Settings::Gui::SearchSuccessFocus>()) {
        m_playlistController->uiController()->focusPlaylist();
    }

    return true;
}

void SearchWidget::handleSearchFailed()
{
    if(m_colours.failBg) {
        QPalette palette = m_searchBox->palette();
        palette.setColor(QPalette::Base, m_colours.failBg.value());
        m_searchBox->setPalette(palette);
    }
    if(m_colours.failFg) {
        QPalette palette = m_searchBox->palette();
        palette.setColor(QPalette::Text, m_colours.failFg.value());
        m_searchBox->setPalette(palette);
    }
}

void SearchWidget::loadColours()
{
    const auto failBg = m_settings->value<Settings::Gui::SearchErrorBg>();
    if(failBg.isNull()) {
        m_colours.failBg = {};
    }
    else {
        m_colours.failBg = failBg.value<QColor>();
    }

    const auto failFg = m_settings->value<Settings::Gui::SearchErrorFg>();
    if(failFg.isNull()) {
        m_colours.failFg = {};
    }
    else {
        m_colours.failFg = failFg.value<QColor>();
    }
}

void SearchWidget::resetColours()
{
    const QPalette defaultPalette = m_searchBox->style()->standardPalette();
    m_searchBox->setPalette(defaultPalette);
}

void SearchWidget::updateConnectedState()
{
    const auto widgets  = m_searchController->connectedWidgets(id());
    m_unconnected       = widgets.empty();
    m_exclusivePlaylist = (widgets.size() == 1 && qobject_cast<PlaylistWidget*>(widgets.front()));

    if(m_unconnected) {
        static const QString searchTooltip
            = u"<b>%1</b><br><br>"
              "<b>Ctrl</b>: %2<br>"
              "<b>Alt</b>: %3<br>"
              "<b>Shift</b>: %4<br>"
              "<b>Ctrl + Alt</b>: %5<br>"
              "<b>Ctrl + Alt + Shift</b>: %6<br>"
              "<b>Ctrl + Backspace</b>: %7<br>"_s.arg(
                  tr("Special Keys"), tr("Force the creation of a new results playlist"),
                  tr("Force search in the current playlist"), tr("Force search in all playlists"),
                  tr("Force new results playlist using the current playlist as the source"),
                  tr("Force new results playlist using all playlists"), tr("Delete a word in the search box"));
        m_searchBox->setToolTip(searchTooltip);
    }
    else {
        m_searchBox->setToolTip({});
    }
}

SearchRequest SearchWidget::currentSearchRequest() const
{
    return {.text = m_searchBox->text(), .emptyMode = m_showAll ? EmptySearchMode::ShowAll : EmptySearchMode::Clear};
}

void SearchWidget::searchChanged(bool enterKey)
{
    if(!m_unconnected) {
        m_searchController->changeSearch(id(), currentSearchRequest());
        return;
    }

    const auto mode             = m_forceMode ? std::exchange(m_forceMode, {}).value() : m_mode; // NOLINT
    const auto request          = currentSearchRequest();
    const uint64_t requestToken = ++m_searchRequestToken;

    Utils::asyncExec([search = request.text, emptyMode = request.emptyMode, tracks = getTracksToSearch(mode)]() {
        if(search.isEmpty()) {
            return emptyMode == EmptySearchMode::ShowAll ? tracks : PlaylistTrackList{};
        }
        TrackQueryFilter filter;
        return filter.filter(search, tracks);
    }).then(this, [this, mode, request, requestToken, enterKey](const PlaylistTrackList& filteredTracks) {
        const SearchRequest currentRequest = currentSearchRequest();
        if(requestToken != m_searchRequestToken || currentRequest.text != request.text
           || currentRequest.emptyMode != request.emptyMode) {
            return;
        }

        if(handleFilteredTracks(mode, filteredTracks) && enterKey) {
            if(isQuickSearch() && m_settings->value<Settings::Gui::SearchSuccessClose>()) {
                close();
            }
            else if(m_settings->value<Settings::Gui::SearchSuccessClear>()) {
                m_searchBox->clear();
            }
        }
    });
}

void SearchWidget::changePlaceholderText()
{
    auto* lineEdit = new PopupLineEdit(m_searchBox->placeholderText(), this);
    lineEdit->setAttribute(Qt::WA_DeleteOnClose);

    QObject::connect(lineEdit, &PopupLineEdit::editingCancelled, lineEdit, &QWidget::close);
    QObject::connect(lineEdit, &PopupLineEdit::editingFinished, this, [this, lineEdit]() {
        const QString text = lineEdit->text();
        if(text != m_searchBox->placeholderText()) {
            m_searchBox->setPlaceholderText(!text.isEmpty() ? text : m_defaultPlaceholder);
        }
        lineEdit->close();
    });

    lineEdit->setGeometry(m_searchBox->geometry());
    lineEdit->show();
    lineEdit->selectAll();
    lineEdit->setFocus(Qt::ActiveWindowFocusReason);
}

void SearchWidget::showOptionsMenu()
{
    auto* menu = new QMenu(tr("Options"), this);
    menu->setAttribute(Qt::WA_DeleteOnClose);

    auto* autoSearch = new QAction(tr("Autosearch"), menu);
    autoSearch->setCheckable(true);
    autoSearch->setChecked(m_autoSearch);
    QObject::connect(autoSearch, &QAction::triggered, this, [this](const bool checked) { m_autoSearch = checked; });
    menu->addAction(autoSearch);

    auto* showAll = new QAction(tr("Show all when search is empty"), this);
    showAll->setCheckable(true);
    showAll->setChecked(m_showAll);
    QObject::connect(showAll, &QAction::triggered, this, [this](const bool checked) {
        m_showAll = checked;
        searchChanged();
    });
    menu->addAction(showAll);

    if(m_unconnected) {
        auto* searchInMenu = menu->addMenu(tr("Search in"));

        auto* searchLibrary = new QAction(tr("Library"), menu);
        searchLibrary->setCheckable(true);
        searchLibrary->setChecked(m_mode == SearchMode::Library);
        QObject::connect(searchLibrary, &QAction::triggered, this, [this]() { m_mode = SearchMode::Library; });
        searchInMenu->addAction(searchLibrary);

        auto* searchPlaylist = new QAction(tr("Playlist"), menu);
        searchPlaylist->setCheckable(true);
        searchPlaylist->setChecked(m_mode == SearchMode::Playlist);
        QObject::connect(searchPlaylist, &QAction::triggered, this, [this]() { m_mode = SearchMode::Playlist; });
        searchInMenu->addAction(searchPlaylist);

        auto* searchAllPlaylist = new QAction(tr("All Playlists"), menu);
        searchAllPlaylist->setCheckable(true);
        searchAllPlaylist->setChecked(m_mode == SearchMode::AllPlaylists);
        QObject::connect(searchAllPlaylist, &QAction::triggered, this, [this]() { m_mode = SearchMode::AllPlaylists; });
        searchInMenu->addAction(searchAllPlaylist);

        menu->addSeparator();
    }

    if(!isQuickSearch()) {
        // Quick search widget can't be connected to other widgets
        auto* changePlaceholder = new QAction(tr("Change placeholder text"), menu);
        QObject::connect(changePlaceholder, &QAction::triggered, this, &SearchWidget::changePlaceholderText);
        menu->addAction(changePlaceholder);

        auto* manageConnections = new QAction(tr("Manage connected widgets"), menu);
        QObject::connect(manageConnections, &QAction::triggered, this,
                         [this]() { m_searchController->setupWidgetConnections(id()); });
        menu->addAction(manageConnections);
    }

    auto* searching = new QAction(tr("Help"), menu);
    QObject::connect(searching, &QAction::triggered, this,
                     []() { QDesktopServices::openUrl(u"https://docs.fooyin.org/en/latest/searching/basics.html"_s); });
    menu->addAction(searching);

    QStyleOptionFrame opt;
    opt.initFrom(m_searchBox);

    const QRect rect = m_searchBox->style()->subElementRect(QStyle::SE_LineEditContents, &opt, m_searchBox);
    const QPoint pos{rect.right() - 5, rect.center().y()};

    menu->popup(m_searchBox->mapToGlobal(pos));
}
} // namespace Fooyin

#include "moc_searchwidget.cpp"
