// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0

#include "signalslotdialog_p.h"
#include "ui_signalslotdialog.h"
#include "metadatabase_p.h"
#include "widgetdatabase_p.h"

#include "qdesigner_formwindowcommand_p.h"
#include "iconloader_p.h"

#include <QtDesigner/membersheet.h>
#include <QtDesigner/qextensionmanager.h>
#include <QtDesigner/abstractformeditor.h>
#include <QtDesigner/abstractformwindow.h>
#include <QtDesigner/abstractwidgetfactory.h>
#include <abstractdialoggui_p.h>

#include <QtGui/qstandarditemmodel.h>
#include <QtGui/qvalidator.h>
#include <QtWidgets/qitemdelegate.h>
#include <QtWidgets/qlineedit.h>
#include <QtWidgets/qapplication.h>
#include <QtWidgets/qmessagebox.h>

#include <QtCore/qregularexpression.h>
#include <QtCore/qdebug.h>

QT_BEGIN_NAMESPACE

using namespace Qt::StringLiterals;

// Regexp to match a function signature, arguments potentially
// with namespace colons.
static constexpr auto signatureRegExp = "^[\\w+_]+\\(([\\w+:]\\*?,?)*\\)$"_L1;
static constexpr auto methodNameRegExp = "^[\\w+_]+$"_L1;

static QStandardItem *createEditableItem(const QString &text)
{
    QStandardItem *rc = new QStandardItem(text);
    rc->setFlags(Qt::ItemIsEnabled|Qt::ItemIsEditable|Qt::ItemIsSelectable);
    return rc;
}

static QStandardItem *createDisabledItem(const QString &text)
{
    QStandardItem *rc = new QStandardItem(text);
    Qt::ItemFlags flags = rc->flags();
    rc->setFlags(flags & ~(Qt::ItemIsEnabled|Qt::ItemIsEditable|Qt::ItemIsSelectable));
    return rc;
}

namespace {
    // Internal helper class: A Delegate that validates using RegExps and additionally checks
    // on closing (adds missing parentheses).
    class SignatureDelegate : public QItemDelegate {
    public:
        SignatureDelegate(QObject * parent = nullptr);
        QWidget * createEditor (QWidget * parent, const QStyleOptionViewItem &option, const QModelIndex &index ) const override;
        void setModelData (QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override;

    private:
        const QRegularExpression m_signatureRegexp;
        const QRegularExpression m_methodNameRegexp;
    };

    SignatureDelegate::SignatureDelegate(QObject * parent) :
        QItemDelegate(parent),
        m_signatureRegexp(signatureRegExp),
        m_methodNameRegexp(methodNameRegExp)
    {
        Q_ASSERT(m_signatureRegexp.isValid());
        Q_ASSERT(m_methodNameRegexp.isValid());
    }

    QWidget * SignatureDelegate::createEditor ( QWidget * parent, const QStyleOptionViewItem &option, const QModelIndex &index ) const
    {
        QWidget *rc = QItemDelegate::createEditor(parent, option, index);
        QLineEdit *le = qobject_cast<QLineEdit *>(rc);
        Q_ASSERT(le);
        le->setValidator(new QRegularExpressionValidator(m_signatureRegexp, le));
        return rc;
    }

    void SignatureDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index ) const
    {
        QLineEdit *le = qobject_cast<QLineEdit *>(editor);
        Q_ASSERT(le);
        // Did the user just type a name? .. Add parentheses
        QString signature = le->text();
        if (!m_signatureRegexp.match(signature).hasMatch()) {
            if (m_methodNameRegexp.match(signature).hasMatch()) {
                signature += "()"_L1;
                le->setText(signature);
            } else {
                return;
            }
        }
        QItemDelegate::setModelData(editor, model, index);
    }

    // ------ FakeMethodMetaDBCommand: Undo Command to change fake methods in the meta DB.
    class FakeMethodMetaDBCommand : public qdesigner_internal::QDesignerFormWindowCommand {

    public:
        explicit FakeMethodMetaDBCommand(QDesignerFormWindowInterface *formWindow);

        void init(QObject *o,
                  const QStringList &oldFakeSlots, const QStringList &oldFakeSignals,
                  const QStringList &newFakeSlots, const QStringList &newFakeSignals);

        void undo() override
        {
            qdesigner_internal::SignalSlotDialog::fakeMethodsToMetaDataBase(core(), m_object,
                                                  m_oldFakeSlots, m_oldFakeSignals);
        }
        void redo() override
        {
            qdesigner_internal::SignalSlotDialog::fakeMethodsToMetaDataBase(core(), m_object,
                                                  m_newFakeSlots, m_newFakeSignals);
        }

    private:
        QObject *m_object;
        QStringList m_oldFakeSlots;
        QStringList m_oldFakeSignals;
        QStringList m_newFakeSlots;
        QStringList m_newFakeSignals;
    };

    FakeMethodMetaDBCommand::FakeMethodMetaDBCommand(QDesignerFormWindowInterface *formWindow) :
        qdesigner_internal::QDesignerFormWindowCommand(QApplication::translate("Command", "Change signals/slots"), formWindow),
        m_object(nullptr)
     {
     }

    void FakeMethodMetaDBCommand::init(QObject *o,
                                       const QStringList &oldFakeSlots, const QStringList &oldFakeSignals,
                                       const QStringList &newFakeSlots, const QStringList &newFakeSignals)
    {
        m_object = o;
        m_oldFakeSlots   = oldFakeSlots;
        m_oldFakeSignals = oldFakeSignals;
        m_newFakeSlots   = newFakeSlots;
        m_newFakeSignals = newFakeSignals;
    }
}

namespace qdesigner_internal {

//  ------ SignalSlotDialogData
void SignalSlotDialogData::clear()
{
    m_existingMethods.clear();
    m_fakeMethods.clear();
}

// ------ SignatureModel
SignatureModel::SignatureModel(QObject *parent) :
     QStandardItemModel(parent)
{
}

bool SignatureModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
    if (role != Qt::EditRole)
        return QStandardItemModel::setData(index, value, role);
    // check via signal (unless it is the same), in which case we can't be bothered.
    const QStandardItem *item = itemFromIndex(index);
    Q_ASSERT(item);
    const QString signature = value.toString();
    if (item->text() == signature)
        return true;

    bool ok = true;
    emit checkSignature(signature, &ok);
    if (!ok)
        return false;

    return QStandardItemModel::setData(index, value, role);
}

// ------ SignaturePanel
SignaturePanel::SignaturePanel(QObject *parent, QListView *listView, QToolButton *addButton, QToolButton *removeButton, const QString &newPrefix) :
    QObject(parent),
    m_newPrefix(newPrefix),
    m_model(new SignatureModel(this)),
    m_listView(listView),
    m_removeButton(removeButton)
{
    m_removeButton->setEnabled(false);

    connect(addButton, &QAbstractButton::clicked, this, &SignaturePanel::slotAdd);
    connect(m_removeButton, &QAbstractButton::clicked, this, &SignaturePanel::slotRemove);

    m_listView->setModel(m_model);
    SignatureDelegate *delegate = new SignatureDelegate(this);
    m_listView->setItemDelegate(delegate);
    connect(m_model, &SignatureModel::checkSignature,
            this, &SignaturePanel::checkSignature);
    connect(m_listView->selectionModel(), &QItemSelectionModel::selectionChanged,
            this, &SignaturePanel::slotSelectionChanged);
}

void SignaturePanel::slotAdd()
{
    m_listView->selectionModel()->clearSelection();
    // find unique name
    for (int i = 1; ; i++) {
        // Always add number, Avoid setting 'slot' for first entry
        QString newSlot = m_newPrefix + QString::number(i) + u'(';
        // check for function name independent of parameters
        if (m_model->findItems(newSlot, Qt::MatchStartsWith, 0).isEmpty()) {
            newSlot += u')';
            QStandardItem * item = createEditableItem(newSlot);
            m_model->appendRow(item);
            const  QModelIndex index = m_model->indexFromItem (item);
            m_listView->setCurrentIndex (index);
            m_listView->edit(index);
            return;
        }
    }
}

int SignaturePanel::count(const QString &signature) const
{
    return m_model->findItems(signature).size();
}

void SignaturePanel::slotRemove()
{
    const QModelIndexList selectedIndexes = m_listView->selectionModel()->selectedIndexes ();
    if (selectedIndexes.isEmpty())
        return;

    closeEditor();
    // scroll to previous
    if (const int row = selectedIndexes.constFirst().row())
        m_listView->setCurrentIndex (selectedIndexes.constFirst().sibling(row - 1, 0));

    for (auto i = selectedIndexes.size() - 1; i >= 0; --i)
        qDeleteAll(m_model->takeRow(selectedIndexes.at(i).row()));
}

void SignaturePanel::slotSelectionChanged(const QItemSelection &selected, const QItemSelection &)
{
    m_removeButton->setEnabled(!selected.indexes().isEmpty());
}

void SignaturePanel::setData(const SignalSlotDialogData &d)
{
    m_model->clear();

    QStandardItem *lastExisting = nullptr;
    for (const QString &s : d.m_existingMethods) {
        lastExisting = createDisabledItem(s);
        m_model->appendRow(lastExisting);
    }
    for (const QString &s : d.m_fakeMethods)
        m_model->appendRow(createEditableItem(s));
    if (lastExisting)
        m_listView->scrollTo(m_model->indexFromItem(lastExisting));
}

QStringList SignaturePanel::fakeMethods() const
{
    QStringList rc;
    if (const int rowCount = m_model->rowCount())
        for (int  i = 0; i < rowCount; i++) {
            const QStandardItem *item =  m_model->item(i);
            if (item->flags() & Qt::ItemIsEditable)
                rc += item->text();
        }
    return rc;
}

void SignaturePanel::closeEditor()
{
    const QModelIndex idx = m_listView->currentIndex();
    if (idx.isValid())
        m_listView->closePersistentEditor(idx);
}

// ------ SignalSlotDialog

SignalSlotDialog::SignalSlotDialog(QDesignerDialogGuiInterface *dialogGui, QWidget *parent, FocusMode mode) :
    QDialog(parent),
    m_focusMode(mode),
    m_ui(new QT_PREPEND_NAMESPACE(Ui)::SignalSlotDialogClass),
    m_dialogGui(dialogGui)
{
    setModal(true);
    m_ui->setupUi(this);

    const QIcon plusIcon = qdesigner_internal::createIconSet("plus.png"_L1);
    const QIcon minusIcon = qdesigner_internal::createIconSet("minus.png"_L1);
    m_ui->addSlotButton->setIcon(plusIcon);
    m_ui->removeSlotButton->setIcon(minusIcon);
    m_ui->addSignalButton->setIcon(plusIcon);
    m_ui->removeSignalButton->setIcon(minusIcon);

    m_slotPanel = new SignaturePanel(this, m_ui->slotListView, m_ui->addSlotButton,
                                     m_ui->removeSlotButton, u"slot"_s);
    m_signalPanel = new SignaturePanel(this, m_ui->signalListView, m_ui->addSignalButton,
                                       m_ui->removeSignalButton, u"signal"_s);
    connect(m_slotPanel, &SignaturePanel::checkSignature,
            this, &SignalSlotDialog::slotCheckSignature);
    connect(m_signalPanel, &SignaturePanel::checkSignature,
            this, &SignalSlotDialog::slotCheckSignature);

    connect(m_ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
    connect(m_ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);

    switch(m_focusMode) {
    case FocusSlots:
        m_ui->slotListView->setFocus(Qt::OtherFocusReason);
        break;
    case  FocusSignals:
        m_ui->signalListView->setFocus(Qt::OtherFocusReason);
        break;
    }
}

SignalSlotDialog::~SignalSlotDialog()
{
    delete m_ui;
}

void SignalSlotDialog::slotCheckSignature(const QString &signature, bool *ok)
{
    QString errorMessage;
    do {
        if (m_slotPanel->count(signature)) {
            errorMessage = tr("There is already a slot with the signature '%1'.").arg(signature);
            *ok = false;
            break;
        }
        if (m_signalPanel->count(signature)) {
            errorMessage = tr("There is already a signal with the signature '%1'.").arg(signature);
            *ok = false;
            break;
        }
    } while (false);
    if (!*ok)
        m_dialogGui->message(this, QDesignerDialogGuiInterface::SignalSlotDialogMessage,
                             QMessageBox::Warning, tr("%1 - Duplicate Signature").arg(windowTitle()), errorMessage, QMessageBox::Close);
}

QDialog::DialogCode SignalSlotDialog::showDialog(SignalSlotDialogData &slotData, SignalSlotDialogData &signalData)
{
    m_slotPanel->setData(slotData);
    m_signalPanel->setData(signalData);

    const DialogCode rc = static_cast<DialogCode>(exec());
    if (rc == Rejected)
        return rc;

    slotData.m_fakeMethods   = m_slotPanel->fakeMethods();
    signalData.m_fakeMethods = m_signalPanel->fakeMethods();
    return rc;
}

bool SignalSlotDialog::editMetaDataBase(QDesignerFormWindowInterface *fw, QObject *object, QWidget *parent, FocusMode mode)
{
    QDesignerFormEditorInterface *core = fw->core();
    SignalSlotDialog dlg(core->dialogGui(), parent, mode);
    dlg.setWindowTitle(tr("Signals/Slots of %1").arg(object->objectName()));

    SignalSlotDialogData slotData;
    SignalSlotDialogData signalData;

    existingMethodsFromMemberSheet(core, object, slotData.m_existingMethods, signalData.m_existingMethods);
    fakeMethodsFromMetaDataBase(core, object, slotData.m_fakeMethods, signalData.m_fakeMethods);

    const QStringList oldSlots =  slotData.m_fakeMethods;
    const QStringList oldSignals = signalData.m_fakeMethods;

    if (dlg.showDialog(slotData, signalData) == QDialog::Rejected)
        return false;

    if (oldSlots == slotData.m_fakeMethods && oldSignals == signalData.m_fakeMethods)
        return false;

    FakeMethodMetaDBCommand *cmd = new FakeMethodMetaDBCommand(fw);
    cmd->init(object, oldSlots, oldSignals, slotData.m_fakeMethods, signalData.m_fakeMethods);
    fw->commandHistory()->push(cmd);
    return true;
}

bool SignalSlotDialog::editPromotedClass(QDesignerFormEditorInterface *core, const QString &promotedClassName, QWidget *parent, FocusMode mode)
{
    const int index = core->widgetDataBase()->indexOfClassName(promotedClassName);
    if (index == -1)
        return false;

    const QString baseClassName = core->widgetDataBase()->item(index)->extends();
    if (baseClassName.isEmpty())
        return false;

    QWidget *widget = core->widgetFactory()->createWidget(baseClassName, nullptr);
    if (!widget)
        return false;
    const bool rc = editPromotedClass(core, promotedClassName, widget, parent, mode);
    widget->deleteLater();
    return rc;
}

bool SignalSlotDialog::editPromotedClass(QDesignerFormEditorInterface *core, QObject *baseObject, QWidget *parent, FocusMode mode)
{
    if (!baseObject->isWidgetType())
        return false;

    const QString promotedClassName = promotedCustomClassName(core, qobject_cast<QWidget*>(baseObject));
    if (promotedClassName.isEmpty())
        return false;
    return  editPromotedClass(core, promotedClassName, baseObject, parent, mode);
}


bool SignalSlotDialog::editPromotedClass(QDesignerFormEditorInterface *core, const QString &promotedClassName, QObject *object, QWidget *parent, FocusMode mode)
{
    WidgetDataBase *db = qobject_cast<WidgetDataBase *>(core->widgetDataBase());
    if (!db)
        return false;

    const int index = core->widgetDataBase()->indexOfClassName(promotedClassName);
    if (index == -1)
        return false;

    WidgetDataBaseItem* item = static_cast<WidgetDataBaseItem*>(db->item(index));

    SignalSlotDialogData slotData;
    SignalSlotDialogData signalData;

    existingMethodsFromMemberSheet(core, object, slotData.m_existingMethods, signalData.m_existingMethods);
    slotData.m_fakeMethods = item->fakeSlots();
    signalData.m_fakeMethods = item->fakeSignals();

    const QStringList oldSlots =  slotData.m_fakeMethods;
    const QStringList oldSignals = signalData.m_fakeMethods;

    SignalSlotDialog dlg(core->dialogGui(), parent, mode);
    dlg.setWindowTitle(tr("Signals/Slots of %1").arg(promotedClassName));

    if (dlg.showDialog(slotData, signalData) == QDialog::Rejected)
        return false;

    if (oldSlots == slotData.m_fakeMethods && oldSignals == signalData.m_fakeMethods)
        return false;

    item->setFakeSlots(slotData.m_fakeMethods);
    item->setFakeSignals(signalData.m_fakeMethods);

    return true;
}

void SignalSlotDialog::fakeMethodsFromMetaDataBase(QDesignerFormEditorInterface *core, QObject *o,
                                                   QStringList &slotList, QStringList &signalList)
{
    slotList.clear();
    signalList.clear();
    if (auto *metaDB = qobject_cast<qdesigner_internal::MetaDataBase *>(core->metaDataBase())) {
        if (const auto *item = metaDB->metaDataBaseItem(o)) {
            slotList = item->fakeSlots();
            signalList = item->fakeSignals();
        }
    }
}

void SignalSlotDialog::fakeMethodsToMetaDataBase(QDesignerFormEditorInterface *core, QObject *o,
                                                 const QStringList &slotList,
                                                 const QStringList &signalList)
{
    if (auto *metaDB = qobject_cast<qdesigner_internal::MetaDataBase *>(core->metaDataBase())) {
        if (auto *item = metaDB->metaDataBaseItem(o)) {
            item->setFakeSlots(slotList);
            item->setFakeSignals(signalList);
        }
    }
}

void SignalSlotDialog::existingMethodsFromMemberSheet(QDesignerFormEditorInterface *core, QObject *o,
                                                      QStringList &slotList, QStringList &signalList)
{
    slotList.clear();
    signalList.clear();

    auto *msheet = qt_extension<QDesignerMemberSheetExtension*>(core->extensionManager(), o);
    if (!msheet)
        return;

    for (qsizetype i = 0, count = msheet->count(); i < count; ++i) {
        if (msheet->isVisible(i)) {
            if (msheet->isSlot(i))
                slotList += msheet->signature(i);
            else if (msheet->isSignal(i))
                signalList += msheet->signature(i);
        }
    }
}

}

QT_END_NAMESPACE

#include "moc_signalslotdialog_p.cpp"
