// Copyright (C) 2020 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
// Qt-Security score:significant reason:default

#include "qoptionswidget_p.h"

#include <QtWidgets/qitemdelegate.h>
#include <QtWidgets/qlayout.h>
#include <QtWidgets/qlistwidget.h>

QT_BEGIN_NAMESPACE

using namespace Qt::StringLiterals;

class ListWidgetDelegate : public QItemDelegate
{
public:
    ListWidgetDelegate(QWidget *w) : QItemDelegate(w), m_widget(w) {}

    static bool isSeparator(const QModelIndex &index) {
        return index.data(Qt::AccessibleDescriptionRole).toString() == "separator"_L1;
    }
    static void setSeparator(QListWidgetItem *item) {
        item->setData(Qt::AccessibleDescriptionRole, QString::fromLatin1("separator"));
        item->setFlags(item->flags() & ~(Qt::ItemIsSelectable|Qt::ItemIsEnabled));
    }

protected:
    void paint(QPainter *painter, const QStyleOptionViewItem &option,
               const QModelIndex &index) const override
    {
        if (isSeparator(index)) {
            QRect rect = option.rect;
            if (const QAbstractItemView *view = qobject_cast<const QAbstractItemView*>(option.widget))
                rect.setWidth(view->viewport()->width());
            QStyleOption opt;
            opt.rect = rect;
            m_widget->style()->drawPrimitive(QStyle::PE_IndicatorToolBarSeparator, &opt, painter,
                                             m_widget);
        } else {
            QItemDelegate::paint(painter, option, index);
        }
    }

    QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override
    {
        if (isSeparator(index)) {
            int pm = m_widget->style()->pixelMetric(QStyle::PM_DefaultFrameWidth, nullptr, m_widget);
            return {pm, pm};
        }
        return QItemDelegate::sizeHint(option, index);
    }

private:
    QWidget *m_widget;
};

static QStringList subtract(const QStringList &minuend, const QStringList &subtrahend)
{
    QStringList result = minuend;
    for (const QString &str : subtrahend)
        result.removeOne(str);
    return result;
}

QOptionsWidget::QOptionsWidget(QWidget *parent)
    : QWidget(parent)
    , m_noOptionText(tr("No Option"))
    , m_invalidOptionText(tr("Invalid Option"))
{
    m_listWidget = new QListWidget(this);
    m_listWidget->setItemDelegate(new ListWidgetDelegate(m_listWidget));
    QVBoxLayout *layout = new QVBoxLayout(this);
    layout->addWidget(m_listWidget);
    layout->setContentsMargins({});
    connect(m_listWidget, &QListWidget::itemChanged, this, &QOptionsWidget::itemChanged);
}

void QOptionsWidget::setOptions(const QStringList &validOptions, const QStringList &selectedOptions)
{
    m_listWidget->clear();
    m_optionToItem.clear();
    m_itemToOption.clear();

    m_validOptions = validOptions;
    m_validOptions.removeDuplicates();
    std::sort(m_validOptions.begin(), m_validOptions.end());

    m_selectedOptions = selectedOptions;
    m_selectedOptions.removeDuplicates();
    std::sort(m_selectedOptions.begin(), m_selectedOptions.end());

    m_invalidOptions = subtract(m_selectedOptions, m_validOptions);
    const QStringList validSelectedOptions = subtract(m_selectedOptions, m_invalidOptions);
    const QStringList validUnselectedOptions = subtract(m_validOptions, m_selectedOptions);

    for (const QString &option : validSelectedOptions)
        appendItem(option, true, true);

    for (const QString &option : std::as_const(m_invalidOptions))
        appendItem(option, false, true);

    if ((validSelectedOptions.size() + m_invalidOptions.size()) && validUnselectedOptions.size())
        appendSeparator();

    for (const QString &option : validUnselectedOptions) {
        appendItem(option, true, false);
        if (option.isEmpty() && validUnselectedOptions.size() > 1) // special No Option item
            appendSeparator();
    }
}

void QOptionsWidget::setNoOptionText(const QString &text)
{
    if (m_noOptionText == text)
        return;

    m_noOptionText = text;

    // update GUI
    const auto itEnd = m_optionToItem.constEnd();
    for (auto it = m_optionToItem.constBegin(); it != itEnd; ++it) {
        const QString optionName = it.key();
        if (optionName.isEmpty())
            it.value()->setText(optionText(optionName, m_validOptions.contains(optionName)));
    }
}

void QOptionsWidget::setInvalidOptionText(const QString &text)
{
    if (m_invalidOptionText == text)
        return;

    m_invalidOptionText = text;

    // update GUI
    for (const QString &option : std::as_const(m_invalidOptions))
        m_optionToItem.value(option)->setText(optionText(option, false));
}

QString QOptionsWidget::optionText(const QString &optionName, bool valid) const
{
    QString text = optionName;
    if (optionName.isEmpty())
        text = u'[' + m_noOptionText + u']';
    if (!valid)
        text += "\t["_L1 + m_invalidOptionText + u']';
    return text;
}

QListWidgetItem *QOptionsWidget::appendItem(const QString &optionName, bool valid, bool selected)
{
    QListWidgetItem *optionItem = new QListWidgetItem(optionText(optionName, valid), m_listWidget);
    optionItem->setCheckState(selected ? Qt::Checked : Qt::Unchecked);
    m_listWidget->addItem(optionItem);
    m_optionToItem[optionName] = optionItem;
    m_itemToOption[optionItem] = optionName;
    return optionItem;
}

void QOptionsWidget::appendSeparator()
{
    QListWidgetItem *separatorItem = new QListWidgetItem(m_listWidget);
    ListWidgetDelegate::setSeparator(separatorItem);
    m_listWidget->addItem(separatorItem);
}

void QOptionsWidget::itemChanged(QListWidgetItem *item)
{
    const auto it = m_itemToOption.constFind(item);
    if (it == m_itemToOption.constEnd())
        return;

    const QString option = *it;

    if (item->checkState() == Qt::Checked && !m_selectedOptions.contains(option)) {
        m_selectedOptions.append(option);
        std::sort(m_selectedOptions.begin(), m_selectedOptions.end());
    } else if (item->checkState() == Qt::Unchecked && m_selectedOptions.contains(option)) {
        m_selectedOptions.removeOne(option);
    } else {
        return;
    }
    emit optionSelectionChanged(m_selectedOptions);
}

QT_END_NAMESPACE
