#include "window.hpp"

#include "protocols.hpp"
#include "ui_window.h"

#include <QAbstractItemView>
#include <QCheckBox>
#include <QGuiApplication>
#include <QLabel>
#include <QShortcut>
#include <fstream>
#include <sys/socket.h>
#include <unistd.h>
#include <wayland-client-protocol.h>

static pid_t pid_from_fd(const int fd) {
    ucred cred{};
    socklen_t len = sizeof(struct ucred);
    if (getsockopt(fd, SOL_SOCKET, SO_PEERCRED, &cred, &len) == -1) {
        perror("getsockopt failed");
        exit(1);
    }
    return cred.pid;
}

static std::string process_name_from_pid(const pid_t pid) {
    const std::string procpath = QString::asprintf("/proc/%d/comm", pid).toStdString();

    std::ifstream infile(procpath);
    if (infile.is_open()) {
        std::string out;
        std::getline(infile, out);
        infile.close();
        return out;
    }

    if (getenv("container") != nullptr || getenv("SNAP") != nullptr) {
        // running in a flatpak or a snap, most likely
        return "Unknown (Sandboxed)";
    }

    return "Unknown";
}

static void registry_global(void* data, wl_registry*, uint32_t, const char* interface, const uint32_t version) {
    if (std::string(interface).starts_with("wl_")) {
        return;
    }

    auto* window = static_cast<Window*>(data);
    window->newGlobal(interface, version);
}

static void registry_global_remove(void*, wl_registry*, uint32_t) {}

Window::Window(const QNativeInterface::QWaylandApplication* waylandApp, QWidget* parent)
    : QMainWindow(parent), store(ProtocolStore()), ui(std::make_unique<Ui::Window>()), searchBox(ui->toolbar),
      supportBox(ui->toolbar) {
    ui->setupUi(this);

    initTable(UPSTREAM, *ui->upstreamTable);
    initTable(WLROOTS, *ui->wlrootsTable);
    initTable(KDE, *ui->kdeTable);
    initTable(COSMIC, *ui->cosmicTable);
    initTable(WESTON, *ui->westonTable);
    initTable(CHROMEOS, *ui->chromeosTable);
    initTable(DEEPIN, *ui->deepinTable);
    initTable(FROG, *ui->frogTable);
    initTable(GAMESCOPE, *ui->gamescopeTable);
    initTable(PHOSH, *ui->phoshTable);
    initTable(UNKNOWN, *ui->unknownTable);

    for (auto* protocol : store.getProtocols()) {
        addProtocol(*protocol);
    }

    ui->upstreamTable->sortByColumn(1, Qt::AscendingOrder);
    for (auto* table :
        {ui->upstreamTable, ui->wlrootsTable, ui->kdeTable, ui->cosmicTable, ui->westonTable, ui->chromeosTable, ui->frogTable, ui->gamescopeTable, ui->phoshTable}) {
        table->sortByColumn(0, Qt::AscendingOrder);
    }

    auto* display = waylandApp->display();
    const auto fd = wl_display_get_fd(display);
    const auto pid = pid_from_fd(fd);
    const auto pname = process_name_from_pid(pid);

    constexpr auto listener = wl_registry_listener{.global = registry_global, .global_remove = registry_global_remove};

    auto* registry = wl_display_get_registry(display);
    wl_registry_add_listener(registry, &listener, this);
    wl_display_roundtrip(display);

    // Toolbar components
    auto* compositor = new QLabel(ui->toolbar);
    compositor->setText(QString::asprintf("Compositor: %s", pname.c_str()));
    compositor->setTextInteractionFlags(Qt::TextSelectableByMouse);
    compositor->setContentsMargins(4, 0, 0, 0);
    QFont font = compositor->font();
    font.setPointSize(14);
    compositor->setFont(font);
    ui->toolbar->addWidget(compositor);

    std::optional<std::string> helpText = std::nullopt;
    if (pname == "Unknown (Sandboxed)") {
        helpText = {"<nobr>When Waycheck runs in a sandbox, such as</nobr> Flatpak or Snap, "
            "it has no way to determine the process name of the Wayland server."};
    } else if (pname == "Unknown") {
        helpText = {"<nobr>Waycheck could not determine the process</nobr> name of the Wayland server. "
            "If you're seeing this message, please report the issue on our Freedesktop GitLab repository."};
    }

    if (helpText.has_value()) {
        auto* action = new QAction();
        action->setIcon(QIcon::fromTheme(QStringLiteral("help-contextual-symbolic")));
        action->setToolTip(QString::fromStdString(helpText.value()));
        ui->toolbar->addAction(action);
    }

    auto* spacer = new QWidget(ui->toolbar);
    spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
    ui->toolbar->addWidget(spacer);

    searchBox.setPlaceholderText("Search…");
    searchBox.setContentsMargins(4, 0, 4, 0);
    searchBox.setFixedWidth(16 * 14);
    const auto* shortcut = new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_F), &searchBox);
    connect(shortcut, &QShortcut::activated, [this] { searchBox.setFocus(); });

    ui->toolbar->addWidget(&searchBox);

    supportBox.addItem("All protocols", Model::SupportFilters::ALL);
    supportBox.addItem("Supported protocols", Model::SupportFilters::SUPPORTED);
    supportBox.addItem("Unsupported protocols", Model::SupportFilters::UNSUPPORTED);
    ui->toolbar->addWidget(&supportBox);
}

Window::~Window() = default;

void Window::newGlobal(const std::string& interface, const uint32_t version) {
    const auto maybeProtocol = store.protocolForInterface(interface);
    if (!maybeProtocol.has_value()) {
        auto versionItem = new QStandardItem(false);
        versionItem->setCheckState(Qt::Checked);
        versionItem->setText(QString::asprintf("%d", version));

        auto items = QList({new QStandardItem(QString::fromStdString(interface)), versionItem});

        for (const auto item : std::as_const(items)) {
            item->setData(true, Qt::UserRole);
            auto font = item->font();
            font.setWeight(QFont::DemiBold);
            item->setFont(font);
            item->setData(true, Qt::UserRole);
        }

        models[UNKNOWN]->sourceModel.appendRow(items);
        return;
    }

    const auto& [source, status, pretty, name] = maybeProtocol.value();
    const auto& model = models[source];

    const auto column = model->includeStatus ? 2 : 1;
    auto matches = model->sourceModel.findItems(QString::fromStdString(name), Qt::MatchExactly, column);

    for (const auto match : matches) {
        const auto versionItem = model->sourceModel.item(match->row(), column + 1);
        versionItem->setCheckState(Qt::Checked);
        versionItem->setText(QString::asprintf("%d", version));

        for (auto i = 0; i < model->sourceModel.columnCount(); i++) {
            const auto item = model->sourceModel.item(match->row(), i);
            auto font = item->font();
            font.setWeight(QFont::DemiBold);
            item->setFont(font);
            item->setData(true, Qt::UserRole);
        }
    }
}

void Window::addProtocol(const Protocol& protocol) {
    const auto& model = models[protocol.source];

    auto versionItem = new QStandardItem(false);
    versionItem->setCheckState(Qt::Unchecked);
    versionItem->setText("N/A");

    auto items = QList({
        new QStandardItem(QString::fromStdString(protocol.pretty)),
        new QStandardItem(QString::fromStdString(protocol.name)),
        versionItem,
    });
    if (protocol.source == UPSTREAM) {
        auto* statusItem = new QStandardItem(QString::fromStdString(status_to_string(protocol.status)));
        statusItem->setTextAlignment(Qt::AlignCenter);
        items.prepend(statusItem);
    }

    for (const auto item : std::as_const(items)) {
        item->setData(false, Qt::UserRole);
    }

    model->sourceModel.appendRow(items);
}

void Window::initTable(const ProtocolSource source, QTableView& table) {
    models[source] = std::make_shared<Model>(source);
    auto model = models[source];

    table.setModel(model->filteredModel);
    table.setSortingEnabled(true);
    table.setEditTriggers(QAbstractItemView::NoEditTriggers);
    table.setSelectionMode(QAbstractItemView::SingleSelection);
    table.verticalHeader()->setVisible(false);
    table.setItemDelegate(new TableItemDelegate);

    if (source == UPSTREAM) {
        table.horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
        table.horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch);
        table.horizontalHeader()->setSectionResizeMode(2, QHeaderView::ResizeToContents);
        table.horizontalHeader()->setSectionResizeMode(3, QHeaderView::ResizeToContents);
    } else if (source != UNKNOWN) {
        table.horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
        table.horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch);
        table.horizontalHeader()->setSectionResizeMode(2, QHeaderView::ResizeToContents);
    } else {
        table.horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch);
        table.horizontalHeader()->setSectionResizeMode(1, QHeaderView::ResizeToContents);
    }

    table.sortByColumn(0, Qt::AscendingOrder);
    table.setEditTriggers(QAbstractItemView::NoEditTriggers);
    table.setSelectionMode(QAbstractItemView::SingleSelection);
    table.verticalHeader()->setVisible(false);
    table.setTabKeyNavigation(false);
    table.setItemDelegate(new TableItemDelegate);
    table.setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);

    // Connect filters to UI
    connect(&searchBox, &QLineEdit::textChanged, [model](const QString& text) { model->setSearchFilter(text); });

    connect(&supportBox, &QComboBox::currentIndexChanged, [model, this](const int& index) {
        model->setSupportFilter(static_cast<Model::SupportFilters>(supportBox.itemData(index).toInt()));
    });

    // Resize table columns when model is updated
    auto resizeColumns = [&table] {
        table.setVisible(false);
        table.resizeColumnsToContents();
        table.setVisible(true);
    };

    connect(model->filteredModel, &QSortFilterProxyModel::rowsInserted, resizeColumns);
    connect(model->filteredModel, &QSortFilterProxyModel::rowsRemoved, resizeColumns);

    // only allow focus on the currently selected tab
    connect(ui->toolBox, &QTabWidget::currentChanged, [this, &table](const int index) {
        const int this_index = ui->toolBox->indexOf(&table);
        if (index == this_index) {
            table.setFocusPolicy(Qt::StrongFocus);
        } else {
            table.setFocusPolicy(Qt::NoFocus);
        }
    });

    // Set tab title
    auto titleTab = [this, model, &table](bool filtering) {
        const int index = ui->toolBox->indexOf(&table);
        const int count = model->filteredModel->rowCount();

        const bool tabVisible = ui->toolBox->isTabVisible(index);
        bool tabShouldBeVisible = true;

        if (count == 0) {
            tabShouldBeVisible = false;
        } else if (!filtering) {
            ui->toolBox->setTabText(index, model->name);
        } else {
            ui->toolBox->setTabText(index, QStringLiteral("%1 (%2)").arg(model->name).arg(count));
        }

        if (tabVisible != tabShouldBeVisible) {
            ui->toolBox->setTabVisible(index, tabShouldBeVisible);
        }

        bool tabsVisible = false;
        for (int i = 0; i < ui->toolBox->count(); i++) {
            if (ui->toolBox->isTabVisible(i)) {
                tabsVisible = true;
                break;
            }
        }

        ui->toolBox->setVisible(tabsVisible);
        ui->filterMessage->setVisible(!tabsVisible);
    };

    titleTab(false);
    connect(model.get(), &Model::filterChanged, titleTab);
}

TableItemDelegate::TableItemDelegate(QObject* parent) : QStyledItemDelegate(parent) {}

void TableItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const {
    const bool enabled = index.data(Qt::UserRole).toBool();

    if (!enabled) {
        QStyleOptionViewItem newOption(option);
        initStyleOption(&newOption, index);

        QColor textColor = newOption.palette.text().color();
        textColor.setAlphaF(0.6);
        newOption.palette.setColor(QPalette::Text, textColor);

        QStyledItemDelegate::paint(painter, newOption, index);
    } else {
        QStyledItemDelegate::paint(painter, option, index);
    }
}
