// 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 "formlayoutmenu_p.h"
#include "layoutinfo_p.h"
#include "qdesigner_command_p.h"
#include "qdesigner_utils_p.h"
#include "qdesigner_propertycommand_p.h"
#include "ui_formlayoutrowdialog.h"

#include <QtDesigner/abstractformwindow.h>
#include <QtDesigner/abstractformeditor.h>
#include <QtDesigner/abstractwidgetfactory.h>
#include <QtDesigner/propertysheet.h>
#include <QtDesigner/qextensionmanager.h>
#include <QtDesigner/abstractwidgetdatabase.h>
#include <QtDesigner/abstractlanguage.h>

#include <QtWidgets/qwidget.h>
#include <QtWidgets/qformlayout.h>
#include <QtWidgets/qdialog.h>
#include <QtWidgets/qpushbutton.h>

#include <QtGui/qaction.h>
#include <QtGui/qvalidator.h>
#include <QtGui/qundostack.h>

#include <QtCore/qpair.h>
#include <QtCore/qcoreapplication.h>
#include <QtCore/qregularexpression.h>
#include <QtCore/qhash.h>
#include <QtCore/qdebug.h>

QT_BEGIN_NAMESPACE

using namespace Qt::StringLiterals;

static constexpr auto buddyPropertyC = "buddy"_L1;
static const char *fieldWidgetBaseClasses[] = {
    "QLineEdit", "QComboBox", "QSpinBox", "QDoubleSpinBox", "QCheckBox",
    "QDateEdit", "QTimeEdit", "QDateTimeEdit", "QDial", "QWidget"
};

namespace qdesigner_internal {

// Struct that describes a row of controls (descriptive label and control) to
// be added to a form layout.
struct FormLayoutRow {
    QString labelName;
    QString labelText;
    QString fieldClassName;
    QString fieldName;
    bool buddy{false};
};

// A Dialog to edit a FormLayoutRow. Lets the user input a label text, label
// name, field widget type, field object name and buddy setting. As the
// user types the label text; the object names to be used for label and field
// are updated. It also checks the buddy setting depending on whether  the
// label text contains a buddy marker.
class FormLayoutRowDialog : public QDialog {
    Q_DISABLE_COPY_MOVE(FormLayoutRowDialog)
    Q_OBJECT
public:
    explicit FormLayoutRowDialog(QDesignerFormEditorInterface *core,
                                 QWidget *parent);

    FormLayoutRow formLayoutRow() const;

    bool buddy() const;
    void setBuddy(bool);

    // Accessors for form layout row numbers using 0..[n-1] convention
    int row() const;
    void setRow(int);
    void setRowRange(int, int);

    QString fieldClass() const;
    QString labelText() const;

    static QStringList fieldWidgetClasses(QDesignerFormEditorInterface *core);

private slots:
    void labelTextEdited(const QString &text);
    void labelNameEdited(const QString &text);
    void fieldNameEdited(const QString &text);
    void buddyClicked();
    void fieldClassChanged(int);

private:
    bool isValid() const;
    void updateObjectNames(bool updateLabel, bool updateField);
    void updateOkButton();

    // Check for buddy marker in string
    const QRegularExpression m_buddyMarkerRegexp;

    QT_PREPEND_NAMESPACE(Ui)::FormLayoutRowDialog m_ui;
    bool m_labelNameEdited;
    bool m_fieldNameEdited;
    bool m_buddyClicked;
};

FormLayoutRowDialog::FormLayoutRowDialog(QDesignerFormEditorInterface *core,
                                         QWidget *parent) :
    QDialog(parent),
    m_buddyMarkerRegexp(u"\\&[^&]"_s),
    m_labelNameEdited(false),
    m_fieldNameEdited(false),
    m_buddyClicked(false)
{
    Q_ASSERT(m_buddyMarkerRegexp.isValid());

    setModal(true);
    m_ui.setupUi(this);
    connect(m_ui.labelTextLineEdit, &QLineEdit::textEdited, this, &FormLayoutRowDialog::labelTextEdited);

    auto *nameValidator = new QRegularExpressionValidator(QRegularExpression(u"^[a-zA-Z0-9_]+$"_s), this);
    Q_ASSERT(nameValidator->regularExpression().isValid());

    m_ui.labelNameLineEdit->setValidator(nameValidator);
    connect(m_ui.labelNameLineEdit, &QLineEdit::textEdited,
            this, &FormLayoutRowDialog::labelNameEdited);

    m_ui.fieldNameLineEdit->setValidator(nameValidator);
    connect(m_ui.fieldNameLineEdit, &QLineEdit::textEdited,
            this, &FormLayoutRowDialog::fieldNameEdited);

    connect(m_ui.buddyCheckBox, &QAbstractButton::clicked, this, &FormLayoutRowDialog::buddyClicked);

    m_ui.fieldClassComboBox->addItems(fieldWidgetClasses(core));
    m_ui.fieldClassComboBox->setCurrentIndex(0);
    connect(m_ui.fieldClassComboBox,
            &QComboBox::currentIndexChanged,
            this, &FormLayoutRowDialog::fieldClassChanged);

    updateOkButton();
}

FormLayoutRow FormLayoutRowDialog::formLayoutRow() const
{
    FormLayoutRow rc;
    rc.labelText = labelText();
    rc.labelName = m_ui.labelNameLineEdit->text();
    rc.fieldClassName = fieldClass();
    rc.fieldName = m_ui.fieldNameLineEdit->text();
    rc.buddy = buddy();
    return rc;
}

bool FormLayoutRowDialog::buddy() const
{
    return m_ui.buddyCheckBox->checkState() == Qt::Checked;
}

void FormLayoutRowDialog::setBuddy(bool b)
{
    m_ui.buddyCheckBox->setCheckState(b ? Qt::Checked : Qt::Unchecked);
}

// Convert rows to 1..n convention for users
int FormLayoutRowDialog::row() const
{
    return m_ui.rowSpinBox->value()  - 1;
}

void FormLayoutRowDialog::setRow(int row)
{
    m_ui.rowSpinBox->setValue(row + 1);
}

void FormLayoutRowDialog::setRowRange(int from, int to)
{
    m_ui.rowSpinBox->setMinimum(from + 1);
    m_ui.rowSpinBox->setMaximum(to + 1);
    m_ui.rowSpinBox->setEnabled(to - from > 0);
}

QString FormLayoutRowDialog::fieldClass() const
{
    return m_ui.fieldClassComboBox->itemText(m_ui.fieldClassComboBox->currentIndex());
}

QString FormLayoutRowDialog::labelText() const
{
    return m_ui.labelTextLineEdit->text();
}

bool FormLayoutRowDialog::isValid() const
{
    // Check for non-empty names and presence of buddy marker if checked
    const QString name = labelText();
    if (name.isEmpty() || m_ui.labelNameLineEdit->text().isEmpty() || m_ui.fieldNameLineEdit->text().isEmpty())
        return false;
    if (buddy() && !name.contains(m_buddyMarkerRegexp))
        return false;
    return true;
}

void FormLayoutRowDialog::updateOkButton()
{
    m_ui.buttonBox->button(QDialogButtonBox::Ok)->setEnabled(isValid());
}

void FormLayoutRowDialog::labelTextEdited(const QString &text)
{
    updateObjectNames(true, true);
    // Set buddy if '&' is present unless the user changed it
    if (!m_buddyClicked)
        setBuddy(text.contains(m_buddyMarkerRegexp));

    updateOkButton();
}

// Get a suitable object name postfix from a class name:
// "namespace::QLineEdit"->"LineEdit"
static inline QString postFixFromClassName(QString className)
{
    const int index = className.lastIndexOf("::"_L1);
    if (index != -1)
        className.remove(0, index + 2);
    if (className.size() > 2)
        if (className.at(0) == u'Q' || className.at(0) == u'K')
            if (className.at(1).isUpper())
                className.remove(0, 1);
    return className;
}

// Helper routines to filter out characters for converting texts into
// class name prefixes. Only accepts ASCII characters/digits and underscores.

enum PrefixCharacterKind { PC_Digit, PC_UpperCaseLetter, PC_LowerCaseLetter,
                           PC_Other, PC_Invalid };

static inline PrefixCharacterKind prefixCharacterKind(const QChar &c)
{
    switch (c.category()) {
    case QChar::Number_DecimalDigit:
        return PC_Digit;
    case QChar::Letter_Lowercase: {
            const char a = c.toLatin1();
            if (a >= 'a' && a <= 'z')
                return PC_LowerCaseLetter;
        }
        break;
    case QChar::Letter_Uppercase: {
            const char a = c.toLatin1();
            if (a >= 'A' && a <= 'Z')
                return PC_UpperCaseLetter;
        }
        break;
    case QChar::Punctuation_Connector:
        if (c.toLatin1() == '_')
            return PC_Other;
        break;
    default:
        break;
    }
    return PC_Invalid;
}

// Convert the text the user types into a usable class name prefix by filtering
// characters, lower-casing the first character and camel-casing subsequent
// words. ("zip code:") --> ("zipCode").

static QString prefixFromLabel(const QString &prefix)
{
    QString rc;
    bool lastWasAcceptable = false;
    for (const QChar &c : prefix) {
        const PrefixCharacterKind kind = prefixCharacterKind(c);
        const bool acceptable = kind != PC_Invalid;
        if (acceptable) {
            if (rc.isEmpty()) {
                // Lower-case first character
                rc += kind == PC_UpperCaseLetter ? c.toLower() : c;
            } else {
                // Camel-case words
                rc += !lastWasAcceptable && kind == PC_LowerCaseLetter ? c.toUpper() : c;
            }
        }
        lastWasAcceptable = acceptable;
    }
    return rc;
}

void FormLayoutRowDialog::updateObjectNames(bool updateLabel, bool updateField)
{
    // Generate label + field object names from the label text, that is,
    // "&Zip code:" -> "zipcodeLabel", "zipcodeLineEdit" unless the user
    // edited it.
    const bool doUpdateLabel = !m_labelNameEdited && updateLabel;
    const bool doUpdateField = !m_fieldNameEdited && updateField;
    if (!doUpdateLabel && !doUpdateField)
        return;

    const QString prefix = prefixFromLabel(labelText());
    // Set names
    if (doUpdateLabel)
        m_ui.labelNameLineEdit->setText(prefix + "Label"_L1);
    if (doUpdateField)
        m_ui.fieldNameLineEdit->setText(prefix + postFixFromClassName(fieldClass()));
}

void FormLayoutRowDialog::fieldClassChanged(int)
{
    updateObjectNames(false, true);
}

void FormLayoutRowDialog::labelNameEdited(const QString & /*text*/)
{
    m_labelNameEdited = true; // stop auto-updating after user change
    updateOkButton();
}

void FormLayoutRowDialog::fieldNameEdited(const QString & /*text*/)
{
    m_fieldNameEdited = true; // stop auto-updating after user change
    updateOkButton();
}

void FormLayoutRowDialog::buddyClicked()
{
    m_buddyClicked = true; // stop auto-updating after user change
    updateOkButton();
}

/* Create a list of classes suitable for field widgets. Take the fixed base
 * classes provided and look in the widget database for custom widgets derived
 * from them ("QLineEdit", "CustomLineEdit", "QComboBox"...). */
QStringList FormLayoutRowDialog::fieldWidgetClasses(QDesignerFormEditorInterface *core)
{
    static QStringList rc;
    if (rc.isEmpty()) {
        // Turn known base classes into list
        QStringList baseClasses;
        for (auto fw : fieldWidgetBaseClasses)
            baseClasses.append(QLatin1StringView(fw));
        // Scan for custom widgets that inherit them and store them in a
        // multimap of base class->custom widgets unless we have a language
        // extension installed which might do funny things with custom widgets.
        QMultiHash<QString, QString> customClassMap; // Base class -> custom widgets map
        if (qt_extension<QDesignerLanguageExtension *>(core->extensionManager(), core) == nullptr) {
            const QDesignerWidgetDataBaseInterface *wdb = core->widgetDataBase();
            const int wdbCount = wdb->count();
            for (int w = 0; w < wdbCount; ++w) {
                // Check for non-container custom types that extend the
                // respective base class.
                const QDesignerWidgetDataBaseItemInterface *dbItem = wdb->item(w);
                if (!dbItem->isPromoted() && !dbItem->isContainer() && dbItem->isCustom()) {
                    const int index = baseClasses.indexOf(dbItem->extends());
                    if (index != -1)
                    customClassMap.insert(baseClasses.at(index), dbItem->name());
                }
            }
        }
        // Compile final list, taking each base class and append custom widgets
        // based on it.
        for (const auto &baseClass : baseClasses) {
            rc.append(baseClass);
            rc += customClassMap.values(baseClass);
        }
    }
    return rc;
}

// ------------------ Utilities

static QFormLayout *managedFormLayout(const QDesignerFormEditorInterface *core, const QWidget *w)
{
    QLayout *l = nullptr;
    if (LayoutInfo::managedLayoutType(core, w, &l) == LayoutInfo::Form)
        return qobject_cast<QFormLayout *>(l);
    return nullptr;
}

// Create the widgets of a control row and apply text properties contained
// in the struct, called by addFormLayoutRow()
static std::pair<QWidget *,QWidget *>
        createWidgets(const FormLayoutRow &row, QWidget *parent,
                      QDesignerFormWindowInterface *formWindow)
{
    QDesignerFormEditorInterface *core = formWindow->core();
    QDesignerWidgetFactoryInterface *wf = core->widgetFactory();

    std::pair<QWidget *,QWidget *> rc{wf->createWidget(u"QLabel"_s, parent),
                                      wf->createWidget(row.fieldClassName, parent)};
    // Set up properties of the label
    const QString objectNameProperty = u"objectName"_s;
    QDesignerPropertySheetExtension *labelSheet = qt_extension<QDesignerPropertySheetExtension*>(core->extensionManager(), rc.first);
    int nameIndex = labelSheet->indexOf(objectNameProperty);
    labelSheet->setProperty(nameIndex, QVariant::fromValue(PropertySheetStringValue(row.labelName)));
    labelSheet->setChanged(nameIndex, true);
    formWindow->ensureUniqueObjectName(rc.first);
    const int textIndex = labelSheet->indexOf(u"text"_s);
    labelSheet->setProperty(textIndex, QVariant::fromValue(PropertySheetStringValue(row.labelText)));
    labelSheet->setChanged(textIndex, true);
    // Set up properties of the control
    QDesignerPropertySheetExtension *controlSheet = qt_extension<QDesignerPropertySheetExtension*>(core->extensionManager(), rc.second);
    nameIndex = controlSheet->indexOf(objectNameProperty);
    controlSheet->setProperty(nameIndex, QVariant::fromValue(PropertySheetStringValue(row.fieldName)));
    controlSheet->setChanged(nameIndex, true);
    formWindow->ensureUniqueObjectName(rc.second);
    return rc;
}

// Create a command sequence on the undo stack of the form window that creates
// the widgets of the row and inserts them into the form layout.
static void addFormLayoutRow(const FormLayoutRow &formLayoutRow, int row, QWidget *w,
                               QDesignerFormWindowInterface *formWindow)
{
    QFormLayout *formLayout = managedFormLayout(formWindow->core(), w);
    Q_ASSERT(formLayout);
    QUndoStack *undoStack = formWindow->commandHistory();
    const QString macroName = QCoreApplication::translate("Command", "Add '%1' to '%2'").arg(formLayoutRow.labelText, formLayout->objectName());
    undoStack->beginMacro(macroName);

    // Create a list of widget insertion commands and pass them a cell position
    const auto widgetPair = createWidgets(formLayoutRow, w, formWindow);

    InsertWidgetCommand *labelCmd = new InsertWidgetCommand(formWindow);
    labelCmd->init(widgetPair.first, false, row, 0);
    undoStack->push(labelCmd);
    InsertWidgetCommand *controlCmd = new InsertWidgetCommand(formWindow);
    controlCmd->init(widgetPair.second, false, row, 1);
    undoStack->push(controlCmd);
    if (formLayoutRow.buddy) {
        SetPropertyCommand *buddyCommand = new SetPropertyCommand(formWindow);
        buddyCommand->init(widgetPair.first, buddyPropertyC, widgetPair.second->objectName());
        undoStack->push(buddyCommand);
    }
    undoStack->endMacro();
}

// ---------------- FormLayoutMenu
FormLayoutMenu::FormLayoutMenu(QObject *parent) :
    QObject(parent),
    m_separator1(new QAction(this)),
    m_populateFormAction(new QAction(tr("Add form layout row..."), this)),
    m_separator2(new QAction(this))
{
    m_separator1->setSeparator(true);
    connect(m_populateFormAction, &QAction::triggered, this, &FormLayoutMenu::slotAddRow);
    m_separator2->setSeparator(true);
}

void FormLayoutMenu::populate(QWidget *w, QDesignerFormWindowInterface *fw, ActionList &actions)
{
    switch (LayoutInfo::managedLayoutType(fw->core(), w)) {
    case LayoutInfo::Form:
        if (!actions.isEmpty() && !actions.constLast()->isSeparator())
            actions.push_back(m_separator1);
        actions.push_back(m_populateFormAction);
        actions.push_back(m_separator2);
        m_widget = w;
        break;
    default:
        m_widget = nullptr;
        break;
    }
}

void FormLayoutMenu::slotAddRow()
{
    QDesignerFormWindowInterface *fw = QDesignerFormWindowInterface::findFormWindow(m_widget);
    Q_ASSERT(m_widget && fw);
    const int rowCount = managedFormLayout(fw->core(), m_widget)->rowCount();

    FormLayoutRowDialog dialog(fw->core(), fw);
    dialog.setRowRange(0, rowCount);
    dialog.setRow(rowCount);

    if (dialog.exec() != QDialog::Accepted)
        return;
    addFormLayoutRow(dialog.formLayoutRow(), dialog.row(), m_widget, fw);
}

QAction *FormLayoutMenu::preferredEditAction(QWidget *w, QDesignerFormWindowInterface *fw)
{
    if (LayoutInfo::managedLayoutType(fw->core(), w) == LayoutInfo::Form) {
        m_widget = w;
        return m_populateFormAction;
    }
    return nullptr;
}
}

QT_END_NAMESPACE

#include "formlayoutmenu.moc"

