/*
 * Fooyin
 * Copyright © 2026, 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 "scriptdisplay.h"

#include "scriptdisplayconfigdialog.h"
#include "scripting/scriptcommandhandler.h"
#include "scripting/scriptvariableproviders.h"

#include <core/player/playercontroller.h>
#include <core/playlist/playlisthandler.h>
#include <core/scripting/scriptenvironmenthelpers.h>
#include <core/scripting/scriptparser.h>
#include <core/track.h>
#include <gui/configdialog.h>
#include <gui/guiconstants.h>
#include <gui/guisettings.h>
#include <gui/guiutils.h>
#include <gui/scripting/richtext.h>
#include <gui/scripting/richtextutils.h>
#include <gui/scripting/scriptformatter.h>
#include <gui/widgets/colourbutton.h>
#include <gui/widgets/scriptlineedit.h>
#include <utils/actions/actionmanager.h>
#include <utils/actions/command.h>
#include <utils/actions/widgetcontext.h>
#include <utils/settings/settingsmanager.h>

#include <QAbstractTextDocumentLayout>
#include <QActionGroup>
#include <QContextMenuEvent>
#include <QDesktopServices>
#include <QGridLayout>
#include <QGroupBox>
#include <QHBoxLayout>
#include <QJsonObject>
#include <QMenu>
#include <QPalette>
#include <QScrollBar>
#include <QSignalBlocker>
#include <QTextBrowser>
#include <QTextDocument>
#include <QUrl>

#include <array>

using namespace Qt::StringLiterals;

constexpr auto ScriptKey              = "TextWidget/Script";
constexpr auto FontKey                = "TextWidget/Font";
constexpr auto BackgroundColourKey    = "TextWidget/BackgroundColour";
constexpr auto ForegroundColourKey    = "TextWidget/ForegroundColour";
constexpr auto LinkColourKey          = "TextWidget/LinkColour";
constexpr auto HorizontalAlignmentKey = "TextWidget/HorizontalAlignment";
constexpr auto VerticalAlignmentKey   = "TextWidget/VerticalAlignment";
constexpr auto ScrollBarKey           = "TextWidget/Scrollbar";

namespace {
QFont fontFromString(const QString& fontString)
{
    QFont font;
    if(!fontString.isEmpty()) {
        font.fromString(fontString);
    }
    return font;
}

QString normaliseColour(const QString& colour)
{
    const QColor parsedColour{colour};
    return parsedColour.isValid() ? parsedColour.name(QColor::HexArgb) : QString{};
}

QString alignmentToCss(const int alignment)
{
    if((alignment & Qt::AlignRight) != 0) {
        return u"right"_s;
    }
    if((alignment & Qt::AlignHCenter) != 0) {
        return u"center"_s;
    }
    return u"left"_s;
}
} // namespace

namespace Fooyin {
// Used to make QTextBrowser::setViewportMargins public
class TextBrowser : public QTextBrowser
{
    Q_OBJECT

public:
    using QTextBrowser::QTextBrowser;
    using QTextBrowser::setViewportMargins;
};

ScriptDisplay::ScriptDisplay(PlayerController* playerController, PlaylistHandler* playlistHandler,
                             ScriptCommandHandler* commandHandler, ActionManager* actionManager,
                             SettingsManager* settings, QWidget* parent)
    : FyWidget{parent}
    , m_playerController{playerController}
    , m_playlistHandler{playlistHandler}
    , m_commandHandler{commandHandler}
    , m_actionManager{actionManager}
    , m_settings{settings}
    , m_layout{new QHBoxLayout(this)}
    , m_text{new TextBrowser(this)}
    , m_context{new WidgetContext(this, Context{Id{"Fooyin.Context.ScriptDisplay."}.append(id())}, this)}
    , m_copyAction{new QAction(tr("&Copy"), this)}
    , m_copyCmd{m_actionManager->registerAction(m_copyAction, Constants::Actions::Copy, m_context->context())}
{
    setObjectName(ScriptDisplay::name());
    m_layout->addWidget(m_text, 1);

    m_actionManager->addContextObject(m_context);
    m_scriptParser.addProvider(playlistVariableProvider());

    m_copyCmd->setCategories({tr("Edit")});
    m_copyCmd->setDefaultShortcut(QKeySequence::Copy);

    m_text->setReadOnly(true);
    m_text->setOpenLinks(false);
    m_text->setOpenExternalLinks(false);
    m_text->setFrameShape(QFrame::NoFrame);
    m_text->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
    m_text->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
    m_text->setUndoRedoEnabled(false);
    m_text->setContextMenuPolicy(Qt::NoContextMenu);
    m_text->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored);
    m_text->document()->setDocumentMargin(0);

    applyConfig(defaultConfig());

    QObject::connect(m_copyAction, &QAction::triggered, m_text, &QTextBrowser::copy);
    QObject::connect(m_text, &QTextBrowser::copyAvailable, this, &ScriptDisplay::updateActions);

    QObject::connect(m_playerController, &PlayerController::playStateChanged, this, &ScriptDisplay::updateText);
    QObject::connect(m_playerController, &PlayerController::positionChangedSeconds, this, &ScriptDisplay::updateText);
    QObject::connect(m_playerController, &PlayerController::bitrateChanged, this, &ScriptDisplay::updateText);
    QObject::connect(m_playerController, &PlayerController::currentTrackChanged, this, &ScriptDisplay::updateText);
    QObject::connect(m_playerController, &PlayerController::currentTrackUpdated, this, &ScriptDisplay::updateText);
    QObject::connect(m_playerController, &PlayerController::playlistTrackUpdated, this, &ScriptDisplay::updateText);

    m_settings->subscribe<Settings::Gui::RatingFullStarSymbol>(this, &ScriptDisplay::updateText);
    m_settings->subscribe<Settings::Gui::RatingHalfStarSymbol>(this, &ScriptDisplay::updateText);
    m_settings->subscribe<Settings::Gui::RatingEmptyStarSymbol>(this, &ScriptDisplay::updateText);
    m_settings->subscribe<Settings::Gui::ResolvedAppStyle>(this, &ScriptDisplay::updateText);

    QObject::connect(m_text, &QTextBrowser::anchorClicked, this,
                     [this](const QUrl& url) { activateLink(url.toString()); });
    QObject::connect(m_text->verticalScrollBar(), &QScrollBar::rangeChanged, this,
                     &ScriptDisplay::updateViewportAlignment);
    QObject::connect(m_text->document()->documentLayout(), &QAbstractTextDocumentLayout::documentSizeChanged, this,
                     &ScriptDisplay::updateViewportAlignment);

    updateActions();
}

ScriptDisplay::~ScriptDisplay() = default;

QString ScriptDisplay::name() const
{
    return tr("Script Display");
}

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

ScriptDisplay::ConfigData ScriptDisplay::defaultConfig() const
{
    auto config{factoryConfig()};

    config.script              = m_settings->fileValue(ScriptKey, config.script).toString();
    config.font                = m_settings->fileValue(FontKey, config.font).toString();
    config.bgColour            = normaliseColour(m_settings->fileValue(BackgroundColourKey, {}).toString());
    config.fgColour            = normaliseColour(m_settings->fileValue(ForegroundColourKey, {}).toString());
    config.linkColour          = normaliseColour(m_settings->fileValue(LinkColourKey, {}).toString());
    config.horizontalAlignment = m_settings->fileValue(HorizontalAlignmentKey, config.horizontalAlignment).toInt();
    config.verticalAlignment   = m_settings->fileValue(VerticalAlignmentKey, config.verticalAlignment).toInt();
    config.showScrollBar       = m_settings->fileValue(ScrollBarKey, config.showScrollBar).toBool();

    return config;
}

ScriptDisplay::ConfigData ScriptDisplay::factoryConfig()
{
    return {
        .script   = uR"(<sized=3><b>%1</b></size>
$crlf()
<alpha=190>%2</alpha>)"_s.arg(tr("Script Display"), tr("Right-click to configure this panel.")),
        .font     = {},
        .bgColour = {},
        .fgColour = {},
        .linkColour          = {},
        .horizontalAlignment = Qt::AlignLeft,
        .verticalAlignment   = Qt::AlignVCenter,
    };
}

const ScriptDisplay::ConfigData& ScriptDisplay::currentConfig() const
{
    return m_config;
}

void ScriptDisplay::applyConfig(const ConfigData& config)
{
    m_config.script              = config.script;
    m_config.font                = config.font;
    m_config.bgColour            = normaliseColour(config.bgColour);
    m_config.fgColour            = normaliseColour(config.fgColour);
    m_config.linkColour          = normaliseColour(config.linkColour);
    m_config.horizontalAlignment = config.horizontalAlignment;
    m_config.verticalAlignment   = config.verticalAlignment;
    m_config.showScrollBar       = config.showScrollBar;

    applyAppearance();
    updateText();
}

void ScriptDisplay::saveDefaults(const ConfigData& config) const
{
    m_settings->fileSet(ScriptKey, config.script);
    m_settings->fileSet(FontKey, config.font);
    m_settings->fileSet(BackgroundColourKey, normaliseColour(config.bgColour));
    m_settings->fileSet(ForegroundColourKey, normaliseColour(config.fgColour));
    m_settings->fileSet(LinkColourKey, normaliseColour(config.linkColour));
    m_settings->fileSet(HorizontalAlignmentKey, config.horizontalAlignment);
    m_settings->fileSet(VerticalAlignmentKey, config.verticalAlignment);
    m_settings->fileSet(ScrollBarKey, config.showScrollBar);
}

void ScriptDisplay::clearSavedDefaults() const
{
    m_settings->fileRemove(ScriptKey);
    m_settings->fileRemove(FontKey);
    m_settings->fileRemove(BackgroundColourKey);
    m_settings->fileRemove(ForegroundColourKey);
    m_settings->fileRemove(LinkColourKey);
    m_settings->fileRemove(HorizontalAlignmentKey);
    m_settings->fileRemove(VerticalAlignmentKey);
    m_settings->fileRemove(ScrollBarKey);
}

void ScriptDisplay::saveLayoutData(QJsonObject& layout)
{
    saveConfigToLayout(m_config, layout);
}

void ScriptDisplay::loadLayoutData(const QJsonObject& layout)
{
    applyConfig(configFromLayout(layout));
}

QSize ScriptDisplay::sizeHint() const
{
    return m_layout->sizeHint();
}

QSize ScriptDisplay::minimumSizeHint() const
{
    return m_layout->minimumSize();
}

void ScriptDisplay::contextMenuEvent(QContextMenuEvent* event)
{
    auto* menu = new QMenu(this);
    menu->setAttribute(Qt::WA_DeleteOnClose);

    if(m_text->textCursor().hasSelection()) {
        menu->addAction(m_copyCmd->action());
        menu->addSeparator();
    }

    const auto applyAlignment = [this](const auto& update) {
        auto config{currentConfig()};
        update(config);
        applyConfig(config);
    };

    const auto addAlignmentMenu = [this, applyAlignment](QMenu* parent, const QString& title, const int currentValue,
                                                         const auto& entries, const auto& setter) {
        auto* subMenu = parent->addMenu(title);
        auto* group   = new QActionGroup(subMenu);
        group->setExclusive(true);

        for(const auto& [text, value] : entries) {
            auto* action = new QAction(text, subMenu);
            subMenu->addAction(action);
            action->setCheckable(true);
            action->setChecked(currentValue == value);
            group->addAction(action);

            QObject::connect(action, &QAction::triggered, this, [value, applyAlignment, setter]() {
                applyAlignment([value, setter](ConfigData& config) { setter(config, value); });
            });
        }
    };

    auto* showScroll = menu->addAction(tr("Show scrollbar"));
    showScroll->setCheckable(true);
    showScroll->setChecked(m_config.showScrollBar);
    QObject::connect(showScroll, &QAction::triggered, this, [this](bool show) {
        auto config{currentConfig()};
        config.showScrollBar = show;
        applyConfig(config);
    });

    auto* alignMenu = menu->addMenu(tr("Align"));

    addAlignmentMenu(alignMenu, tr("Horizontal"), m_config.horizontalAlignment,
                     std::to_array<std::pair<QString, int>>({
                         {tr("Left"), Qt::AlignLeft},
                         {tr("Centre"), Qt::AlignHCenter},
                         {tr("Right"), Qt::AlignRight},
                     }),
                     [](ConfigData& config, const int value) { config.horizontalAlignment = value; });

    addAlignmentMenu(alignMenu, tr("Vertical"), m_config.verticalAlignment,
                     std::to_array<std::pair<QString, int>>({
                         {tr("Top"), Qt::AlignTop},
                         {tr("Centre"), Qt::AlignVCenter},
                         {tr("Bottom"), Qt::AlignBottom},
                     }),
                     [](ConfigData& config, const int value) { config.verticalAlignment = value; });

    menu->addSeparator();
    addConfigureAction(menu, false);
    menu->popup(event->globalPos());
}

void ScriptDisplay::openConfigDialog()
{
    showConfigDialog(new ScriptDisplayConfigDialog(this, this));
}

void ScriptDisplay::resizeEvent(QResizeEvent* event)
{
    FyWidget::resizeEvent(event);
    updateViewportAlignment();
}

ScriptDisplay::ConfigData ScriptDisplay::configFromLayout(const QJsonObject& layout) const
{
    ConfigData config{defaultConfig()};

    if(layout.contains("Script"_L1)) {
        config.script = layout.value("Script"_L1).toString();
    }
    if(layout.contains("Font"_L1)) {
        config.font = layout.value("Font"_L1).toString();
    }
    if(layout.contains("BackgroundColour"_L1)) {
        config.bgColour = normaliseColour(layout.value("BackgroundColour"_L1).toString());
    }
    if(layout.contains("ForegroundColour"_L1)) {
        config.fgColour = normaliseColour(layout.value("ForegroundColour"_L1).toString());
    }
    if(layout.contains("LinkColour"_L1)) {
        config.linkColour = normaliseColour(layout.value("LinkColour"_L1).toString());
    }
    if(layout.contains("HorizontalAlignment"_L1)) {
        config.horizontalAlignment = layout.value("HorizontalAlignment"_L1).toInt();
    }
    if(layout.contains("VerticalAlignment"_L1)) {
        config.verticalAlignment = layout.value("VerticalAlignment"_L1).toInt();
    }
    if(layout.contains("ShowScrollbar"_L1)) {
        config.showScrollBar = layout.value("ShowScrollbar"_L1).toBool();
    }

    return config;
}

void ScriptDisplay::saveConfigToLayout(const ConfigData& config, QJsonObject& layout)
{
    layout["Script"_L1]              = config.script;
    layout["Font"_L1]                = config.font;
    layout["BackgroundColour"_L1]    = normaliseColour(config.bgColour);
    layout["ForegroundColour"_L1]    = normaliseColour(config.fgColour);
    layout["LinkColour"_L1]          = normaliseColour(config.linkColour);
    layout["HorizontalAlignment"_L1] = config.horizontalAlignment;
    layout["VerticalAlignment"_L1]   = config.verticalAlignment;
    layout["ShowScrollbar"_L1]       = config.showScrollBar;
}

void ScriptDisplay::applyAppearance()
{
    if(m_config.font.isEmpty()) {
        m_text->setFont(QFont{});
    }
    else {
        m_text->setFont(fontFromString(m_config.font));
    }

    if(m_config.bgColour.isEmpty()) {
        setAutoFillBackground(false);
        setPalette(QPalette{});
        QPalette browserPalette = m_text->palette();
        browserPalette.setBrush(QPalette::Base, Qt::NoBrush);
        m_text->setPalette(browserPalette);
        m_text->viewport()->setAutoFillBackground(false);
    }
    else {
        QPalette pal = palette();
        pal.setColor(QPalette::Window, QColor{m_config.bgColour});
        setPalette(pal);
        setAutoFillBackground(true);

        QPalette browserPalette = m_text->palette();
        browserPalette.setColor(QPalette::Base, QColor{m_config.bgColour});
        m_text->setPalette(browserPalette);
        m_text->viewport()->setAutoFillBackground(true);
    }

    m_text->show();
    m_text->setVerticalScrollBarPolicy(m_config.showScrollBar ? Qt::ScrollBarAsNeeded : Qt::ScrollBarAlwaysOff);
    updateViewportAlignment();
}

void ScriptDisplay::updateText()
{
    ScriptFormatter formatter;
    formatter.setBaseFont(m_text->font());
    if(!m_config.fgColour.isEmpty()) {
        formatter.setBaseColour(QColor{m_config.fgColour});
    }

    const Track track      = currentTrack();
    const bool resetScroll = track != m_lastTrack;
    const QString text     = evaluateScript();

    const QColor linkColour = m_config.linkColour.isEmpty() ? QColor{} : QColor{m_config.linkColour};
    const QString body      = richTextToHtml(formatter.evaluate(text), linkColour);
    const QString html      = u"<html><body style=\"margin:0;text-align:%1;\">%2</body></html>"_s.arg(
        alignmentToCss(m_config.horizontalAlignment), body);

    if(html == m_lastHtml) {
        if(resetScroll) {
            m_text->verticalScrollBar()->setValue(0);
            m_lastTrack = track;
        }
        return;
    }

    auto* scrollBar         = m_text->verticalScrollBar();
    const int previousValue = scrollBar->value();
    const int previousMax   = scrollBar->maximum();

    const QSignalBlocker scrollBlocker{scrollBar};
    m_text->setUpdatesEnabled(false);
    m_text->setHtml(html);
    m_lastHtml  = html;
    m_lastTrack = track;

    if(resetScroll) {
        scrollBar->setValue(0);
    }
    else if(previousMax > 0) {
        const int newMax        = scrollBar->maximum();
        const int restoredValue = static_cast<int>((static_cast<qint64>(previousValue) * newMax) / previousMax);
        scrollBar->setValue(restoredValue);
    }

    updateViewportAlignment();
    updateActions();
    m_text->setUpdatesEnabled(true);
    m_text->viewport()->update();
}

void ScriptDisplay::updateActions() const
{
    m_copyAction->setEnabled(m_text->textCursor().hasSelection());
}

void ScriptDisplay::updateViewportAlignment()
{
    int topMargin{0};
    int bottomMargin{0};

    const bool hasVerticalScrollbar = m_text->verticalScrollBar()->maximum() > 0;
    if(!hasVerticalScrollbar) {
        const int availableHeight = m_text->contentsRect().height();
        const int documentHeight  = std::ceil(m_text->document()->documentLayout()->documentSize().height());
        const int extraHeight     = std::max(0, availableHeight - documentHeight - topMargin - bottomMargin);

        if((m_config.verticalAlignment & Qt::AlignBottom) != 0) {
            topMargin += extraHeight;
        }
        else if((m_config.verticalAlignment & Qt::AlignVCenter) != 0) {
            const int topExtra = extraHeight / 2;
            topMargin += topExtra;
            bottomMargin += extraHeight - topExtra;
        }
        else {
            bottomMargin += extraHeight;
        }
    }

    if(auto* label = qobject_cast<TextBrowser*>(m_text)) {
        label->setViewportMargins(0, topMargin, 0, bottomMargin);
    }
}

Track ScriptDisplay::currentTrack() const
{
    if(const Track track = m_playerController->currentTrack(); track.isValid()) {
        return track;
    }

    if(auto* playlist = m_playlistHandler->activePlaylist(); playlist && playlist->currentTrack().isValid()) {
        return playlist->currentTrack();
    }

    return {};
}

Playlist* ScriptDisplay::currentPlaylist() const
{
    if(const PlaylistTrack track = m_playerController->currentPlaylistTrack(); track.playlistId.isValid()) {
        if(auto* playlist = m_playlistHandler->playlistById(track.playlistId)) {
            return playlist;
        }
    }

    return nullptr;
}

QString ScriptDisplay::evaluateScript()
{
    auto contextData
        = makePlaybackScriptContext(m_playerController, currentPlaylist(), TrackListContextPolicy::Fallback, {}, true,
                                    false, Gui::ratingStarSymbols(*m_settings));
    return m_scriptParser.evaluate(m_config.script, currentTrack(), contextData.context);
}

void ScriptDisplay::activateLink(const QString& link) const
{
    const QUrl url = QUrl::fromUserInput(link);
    if(!url.isValid()) {
        return;
    }

    if(url.scheme() == "fooyin"_L1 && url.host() == "command"_L1) {
        if(!m_commandHandler) {
            return;
        }

        const QString commandId = QUrl::fromPercentEncoding(url.path().mid(1).toUtf8());
        if(commandId.isEmpty()) {
            return;
        }

        m_commandHandler->execute(commandId);
        return;
    }

    QDesktopServices::openUrl(url);
}
} // namespace Fooyin

#include "moc_scriptdisplay.cpp"
#include "scriptdisplay.moc"
