#include "sqlqueryview.h"
#include "sqlqueryitemdelegate.h"
#include "sqlquerymodel.h"
#include "sqlqueryitem.h"
#include "common/widgetcover.h"
#include "tsvserializer.h"
#include "iconmanager.h"
#include "multieditor/multieditordialog.h"
#include "uiconfig.h"
#include "dialogs/sortdialog.h"
#include "letos.h"
#include "services/functionmanager.h"
#include "services/notifymanager.h"
#include "windows/editorwindow.h"
#include "cellrendererplugin.h"
#include "mainwindow.h"
#include "common/utils_sql.h"
#include "common/mouseshortcut.h"
#include <QPushButton>
#include <QProgressBar>
#include <QGridLayout>
#include <QDebug>
#include <QFocusEvent>
#include <QApplication>
#include <QClipboard>
#include <QAction>
#include <QMenu>
#include <QMimeData>
#include <QCryptographicHash>
#include <QMessageBox>
#include <QScrollBar>
#include <QScopedValueRollback>
#include <services/pluginmanager.h>
#include <multieditor/multieditorwidgetplugin.h>

CFG_KEYS_DEFINE(SqlQueryView)

SqlQueryView::SqlQueryView(QWidget *parent) :
    QTableView(parent)
{
    init();
}

SqlQueryView::~SqlQueryView()
{
    delete itemDelegate;
}

void SqlQueryView::init()
{
    itemDelegate = new SqlQueryItemDelegate();
    setItemDelegate(itemDelegate);
    setMouseTracking(true);
    setReadOnly(false);

    setContextMenuPolicy(Qt::CustomContextMenu);
    contextMenu = new QMenu(this);
    referencedTablesMenu = new QMenu(tr("Go to referenced row in..."), contextMenu);

    setHorizontalHeader(new Header(this));

    connect(this, &QWidget::customContextMenuRequested, this, &SqlQueryView::customContextMenuRequested);
    connect(CFG_UI.Fonts.DataView, SIGNAL(changed(QVariant)), this, SLOT(updateFont()));
    connect(this, SIGNAL(activated(QModelIndex)), this, SLOT(itemActivated(QModelIndex)));
    connect(horizontalHeader(), &QHeaderView::sectionResized, this, [this](int section, int, int newSize)
    {
        if (ignoreColumnWidthChanges)
            return;

        getModel()->setDesiredColumnWidth(section, newSize);
    });
    connect(verticalHeader(), SIGNAL(sectionDoubleClicked(int)), this, SLOT(adjustRowToContents(int)));
    MouseShortcut::forWheel(Qt::ControlModifier,
                            this, SLOT(fontSizeChangeRequested(int)),
                            viewport());

    connect(PLUGINS, SIGNAL(aboutToUnload(Plugin*,PluginType*)), this, SLOT(handlePluginUnload(Plugin*,PluginType*)));
    connect(PLUGINS, SIGNAL(loaded(Plugin*,PluginType*)), this, SLOT(handlePluginLoaded(Plugin*,PluginType*)));

    horizontalHeader()->setSortIndicatorShown(false);
    horizontalHeader()->setSectionsClickable(true);
    updateFont();

    setupWidgetCover();
    initActions();
    setupHeaderMenu();
}

void SqlQueryView::initPinnedView(QAbstractItemModel* model)
{
    pinnedView = new QTableView(this);
    pinnedView->setModel(model);
    pinnedView->setSelectionModel(selectionModel());

    pinnedView->setFocusPolicy(Qt::NoFocus);
    pinnedView->verticalHeader()->hide();
    pinnedView->horizontalHeader()->setSectionResizeMode(QHeaderView::Interactive);
    pinnedView->horizontalHeader()->setStretchLastSection(true);
    pinnedView->horizontalHeader()->setSectionsClickable(true);
    pinnedView->setContextMenuPolicy(Qt::CustomContextMenu);

    viewport()->stackUnder(pinnedView);

    pinnedViewItemDelegate = new SqlQueryItemDelegate();
    pinnedView->setItemDelegate(pinnedViewItemDelegate);

    pinnedView->setStyleSheet(QString(
                            "QTableView {"
                            "     border-top: none;"
                            "     border-left: none;"
                            "     border-bottom: none;"
                            "     border-right-width: 2px;"
                            "     border-right-style: dashed;"
                            "     border-right-color: %1;"
                            "}").arg(pinnedView->palette().color(QPalette::Highlight).name()));
    pinnedView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
    pinnedView->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
    pinnedView->show();

    updatePinnedViewGeometry();

    setHorizontalScrollMode(ScrollPerPixel);
    setVerticalScrollMode(ScrollPerPixel);
    pinnedView->setVerticalScrollMode(ScrollPerPixel);

    connect(horizontalHeader(), &QHeaderView::sectionResized, this, &SqlQueryView::syncPinnedSectionWidth);
    connect(pinnedView->horizontalHeader(), &QHeaderView::sectionResized, this, &SqlQueryView::pinnedSectionResized);
    connect(verticalHeader(), &QHeaderView::sectionResized, this, &SqlQueryView::syncPinnedSectionHeight);

    connect(pinnedView->verticalScrollBar(), &QAbstractSlider::valueChanged, verticalScrollBar(), &QAbstractSlider::setValue);
    connect(verticalScrollBar(), &QAbstractSlider::valueChanged, pinnedView->verticalScrollBar(), &QAbstractSlider::setValue);

    connect(pinnedView, &QWidget::customContextMenuRequested, this, &SqlQueryView::customContextMenuRequested);

    pinnedView->horizontalHeader()->setContextMenuPolicy(Qt::CustomContextMenu);
    connect(pinnedView->horizontalHeader(), &QWidget::customContextMenuRequested, this, &SqlQueryView::headerContextMenuRequested);
}

void SqlQueryView::setupWidgetCover()
{
    widgetCover = new WidgetCover(this);
    widgetCover->initWithInterruptContainer();
}

void SqlQueryView::createActions()
{
    createAction(CUT, ICONS.ACT_CUT, tr("Cut"), this, SLOT(cut()), this);
    createAction(COPY, ICONS.ACT_COPY, tr("Copy"), this, SLOT(copy()), this);
    createAction(COPY_WITH_HEADER, ICONS.ACT_COPY, tr("Copy with headers"), this, SLOT(copyWithHeader()), this);
    createAction(COPY_AS, ICONS.ACT_COPY, tr("Copy as..."), this, SLOT(copyAs()), this);
    createAction(PASTE, ICONS.ACT_PASTE, tr("Paste"), this, SLOT(paste()), this);
    createAction(PASTE_AS, ICONS.ACT_PASTE, tr("Paste as..."), this, SLOT(pasteAs()), this);
    createAction(SET_NULL, ICONS.SET_NULL, tr("Set NULL value"), this, SLOT(setNull()), this);
    createAction(ERASE, ICONS.ERASE, tr("Erase value"), this, SLOT(erase()), this);
    createAction(RESET_VALUE, ICONS.ACT_UNDO, tr("Reset value"), this, SLOT(resetValue()), this);
    createAction(OPEN_VALUE_EDITOR, ICONS.OPEN_VALUE_EDITOR, "", this, SLOT(openValueEditor()), this); // actual label is set dynamically in setupActionsForMenu()
    createAction(COMMIT, ICONS.COMMIT, tr("Commit"), this, SLOT(commit()), this);
    createAction(ROLLBACK, ICONS.ROLLBACK, tr("Rollback"), this, SLOT(rollback()), this);
    createAction(SELECTIVE_COMMIT, ICONS.COMMIT, tr("Commit selected cells"), this, SLOT(selectiveCommit()), this);
    createAction(SELECTIVE_ROLLBACK, ICONS.ROLLBACK, tr("Rollback selected cells"), this, SLOT(selectiveRollback()), this);
    createAction(EDIT_CURRENT, tr("Edit current cell inline"), this, SLOT(editCurrent()), this);
    createAction(GENERATE_SELECT, "SELECT", this, SLOT(generateSelect()), this);
    createAction(GENERATE_INSERT, "INSERT", this, SLOT(generateInsert()), this);
    createAction(GENERATE_UPDATE, "UPDATE", this, SLOT(generateUpdate()), this);
    createAction(GENERATE_DELETE, "DELETE", this, SLOT(generateDelete()), this);
    createAction(SORT_DIALOG, ICONS.SORT_COLUMNS, tr("Define columns to sort by"), this, SLOT(openSortDialog()), this);
    createAction(RESET_SORTING, ICONS.SORT_RESET, tr("Remove custom sorting (Middle click)"), this, SLOT(resetSorting()), this);
    createAction(INSERT_ROW, ICONS.INSERT_ROW, tr("Insert row"), this, SIGNAL(requestForRowInsert()), this);
    createAction(INSERT_MULTIPLE_ROWS, ICONS.INSERT_ROWS, tr("Insert multiple rows"), this, SIGNAL(requestForMultipleRowInsert()), this);
    createAction(DELETE_ROW, ICONS.DELETE_ROW, tr("Delete selected row"), this, SIGNAL(requestForRowDelete()), this);
    createAction(ADJUST_ROWS_SIZE, tr("Adjust height of rows"), this, SLOT(toggleRowsHeightAdjustment(bool)), this);
    actionMap[ADJUST_ROWS_SIZE]->setCheckable(true);
    actionMap[ADJUST_ROWS_SIZE]->setChecked(false);
    createAction(NEXT_ROW_ON_ENTER, tr("Next row on Enter"), this, SLOT(toggleNextRowOnEnter(bool)), this);
    actionMap[NEXT_ROW_ON_ENTER]->setCheckable(true);
    actionMap[NEXT_ROW_ON_ENTER]->setChecked(CFG_UI.General.DataEditAutoAdvanceOnEnter.get());
    actionMap[RESET_SORTING]->setEnabled(false);
    createAction(INCR_FONT_SIZE, tr("Increase font size", "data view"), this, SLOT(incrFontSize()), this);
    createAction(DECR_FONT_SIZE, tr("Decrease font size", "data view"), this, SLOT(decrFontSize()), this);
    createAction(INVERT_SELECTION, ICONS.SELECTION_INVERT, tr("Invert selection", "data view"), this, SLOT(invertSelection()), this);
}

void SqlQueryView::setupDefShortcuts()
{
    setShortcutContext({ROLLBACK, SET_NULL, ERASE, OPEN_VALUE_EDITOR, COMMIT, CUT, COPY, COPY_AS,
                       PASTE, PASTE_AS, ADJUST_ROWS_SIZE, INCR_FONT_SIZE, DECR_FONT_SIZE}, Qt::WidgetWithChildrenShortcut);

    BIND_SHORTCUTS(SqlQueryView, Action);
}

void SqlQueryView::setupActionsForMenu(SqlQueryItem* currentItem, const QList<SqlQueryItem*>& selectedItems)
{
    // Selected items count
    int selCount = selectedItems.size();

    // Uncommitted items count
    QList<SqlQueryItem*> uncommittedItems = getModel()->getUncommittedItems();
    int uncommittedCount = uncommittedItems.size();

    // How many of selected items is editable
    int editableSelCount = selCount;
    for (SqlQueryItem* selItem : getSelectedItems())
        if (selItem->getColumn()->editionForbiddenReason.size() > 0)
            editableSelCount--;

    bool currentItemEditable = (currentItem && currentItem->getColumn()->editionForbiddenReason.size() == 0);

    // Uncommitted & selected items count
    int uncommittedSelCount = 0;
    for (SqlQueryItem* item : uncommittedItems)
        if (selectedItems.contains(item))
            uncommittedSelCount++;

    if (uncommittedCount > 0)
        contextMenu->addAction(actionMap[COMMIT]);

    if (uncommittedSelCount > 0)
        contextMenu->addAction(actionMap[SELECTIVE_COMMIT]);

    if (uncommittedCount > 0)
        contextMenu->addAction(actionMap[ROLLBACK]);

    if (uncommittedSelCount > 0)
        contextMenu->addAction(actionMap[SELECTIVE_ROLLBACK]);

    if (uncommittedCount > 0 && selCount > 0)
        contextMenu->addSeparator();

    // Edit/show label for "open in editor" action
    actionMap[OPEN_VALUE_EDITOR]->setText(currentItemEditable ? tr("Edit value in editor") : tr("Show value in a viewer"));

    if (selCount > 0)
    {
        if (editableSelCount > 0)
        {
            contextMenu->addAction(actionMap[ERASE]);
            contextMenu->addAction(actionMap[SET_NULL]);

            int possibleResetCount = 0;
            for (SqlQueryItem* item : selectedItems)
                if (item->isNewRow() && !item->isUntouched() && item->getColumn()->hasDefaultValueForInsert())
                    possibleResetCount++;

            if (possibleResetCount > 0)
                contextMenu->addAction(actionMap[RESET_VALUE]);
        }
        contextMenu->addAction(actionMap[OPEN_VALUE_EDITOR]);
        contextMenu->addSeparator();
    }

    if (selCount == 1 && currentItem && selectedItems.first() == currentItem)
        addFkActionsToContextMenu(currentItem);

    if (selCount > 0)
    {
        QMenu* generateQueryMenu = contextMenu->addMenu(ICONS.GENERATE_QUERY, tr("Generate query for selected cells"));
        generateQueryMenu->addAction(actionMap[GENERATE_SELECT]);

        Db* db = getModel()->getDb();
        if (db && db->isValid())
        {
            QList<FunctionManager::ScriptFunction*> functions = FUNCTIONS->getScriptFunctionsForDatabase(db->getName());
            if (functions.size() > 0)
            {
                QStringList fnNames;
                // Offer functions with undefined arguments or at least 1 defined argument
                for (FunctionManager::ScriptFunction* fn : functions)
                    if (fn->undefinedArgs || fn->arguments.size() >= 1)
                        fnNames << fn->name;
                fnNames.sort();
                QMenu* generateSelectFunctionMenu = generateQueryMenu->addMenu("SELECT function(...)");
                for (const QString& name : fnNames)
                    generateSelectFunctionMenu->addAction(name, this, SLOT(generateSelectFunction()));
            }
        }

        if (getModel()->supportsModifyingQueriesInMenu())
        {
            generateQueryMenu->addAction(actionMap[GENERATE_INSERT]);
            generateQueryMenu->addAction(actionMap[GENERATE_UPDATE]);
            generateQueryMenu->addAction(actionMap[GENERATE_DELETE]);
        }

        contextMenu->addSeparator();
        contextMenu->addAction(actionMap[CUT]);
        contextMenu->addAction(actionMap[COPY]);
        contextMenu->addAction(actionMap[COPY_WITH_HEADER]);
        //contextMenu->addAction(actionMap[COPY_AS]); // TODO uncomment when implemented
        contextMenu->addAction(actionMap[PASTE]);
        //contextMenu->addAction(actionMap[PASTE_AS]); // TODO uncomment when implemented
    }
    contextMenu->addSeparator();
    contextMenu->addAction(actionMap[INVERT_SELECTION]);
    contextMenu->addAction(actionMap[ADJUST_ROWS_SIZE]);
    contextMenu->addAction(actionMap[NEXT_ROW_ON_ENTER]);
    if (additionalActions.size() > 0)
    {
        contextMenu->addSeparator();
        for (QAction*& action : additionalActions)
            contextMenu->addAction(action);
    }
}

void SqlQueryView::setupHeaderMenu()
{
    horizontalHeader()->setContextMenuPolicy(Qt::CustomContextMenu);
    connect(horizontalHeader(), &QWidget::customContextMenuRequested, this, &SqlQueryView::headerContextMenuRequested);
    headerContextMenu = new QMenu(horizontalHeader());
}

QList<SqlQueryItem*> SqlQueryView::getSelectedItems()
{
    QList<SqlQueryItem*> items;
    QModelIndexList idxList = selectionModel()->selectedIndexes();
    QModelIndex currIdx = getCurrentIndex();
    if (!idxList.contains(currIdx) && currIdx.isValid())
        idxList << currIdx;

    if (idxList.size() == 0)
        return items;

    std::sort(idxList.begin(), idxList.end());
    const SqlQueryModel* model = dynamic_cast<const SqlQueryModel*>(idxList.first().model());
    for (const QModelIndex& idx : idxList)
        items << model->itemFromIndex(idx);

    return items;
}

SqlQueryItem* SqlQueryView::getCurrentItem()
{
    QModelIndex idx = getCurrentIndex();
    if (!idx.isValid())
        return nullptr;

    return getModel()->itemFromIndex(idx);
}

SqlQueryModel* SqlQueryView::getModel()
{
    return dynamic_cast<SqlQueryModel*>(model());
}

void SqlQueryView::setModel(QAbstractItemModel* model)
{
    QTableView::setModel(model);
    SqlQueryModel* m = getModel();
    connect(widgetCover, SIGNAL(cancelClicked()), m, SLOT(interrupt()));
    connect(m, &SqlQueryModel::commitStatusChanged, this, &SqlQueryView::updateCommitRollbackActions);
    connect(m, &SqlQueryModel::sortingUpdated, this, &SqlQueryView::sortingUpdated);
    connect(m, &SqlQueryModel::executionSuccessful, this, [this]()
    {
        if (actionMap[ADJUST_ROWS_SIZE]->isChecked())
            verticalHeader()->resizeSections(QHeaderView::ResizeToContents);

        updatePinnedViewColumns();
        updatePinnedViewRowSizes();
    });

    initPinnedView(model);
}

SqlQueryItem* SqlQueryView::itemAt(const QPoint& pos)
{
    return dynamic_cast<SqlQueryItem*>(getModel()->itemFromIndex(indexAt(pos)));
}

QToolBar* SqlQueryView::getToolBar(int toolbar) const
{
    Q_UNUSED(toolbar);
    return nullptr;
}

void SqlQueryView::addAdditionalAction(QAction* action)
{
    additionalActions << action;
}

void SqlQueryView::addHeaderAdditionalAction(QAction *action)
{
    headerAdditionalActions << action;
}

QModelIndex SqlQueryView::getCurrentIndex() const
{
    return currentIndex();
}

void SqlQueryView::itemActivated(const QModelIndex& index)
{
    if (simpleBrowserMode)
        return;

    if (!index.isValid())
        return;

    SqlQueryItem* item = getModel()->itemFromIndex(index);
    if (!item)
        return;

    if (!editInEditorIfNecessary(item))
        return;

    item->skipInitialFocusSelection();
    if (!simpleBrowserMode)
        edit(getCurrentIndex());
}

void SqlQueryView::generateSelect()
{
    QString sql = getModel()->generateSelectQueryForItems(getSelectedItems());
    MAINWINDOW->openSqlEditor(getModel()->getDb(), sql);
}

void SqlQueryView::generateSelectFunction()
{
    QString function = reinterpret_cast<QAction*>(sender())->text();
    QString sql = getModel()->generateSelectFunctionQueryForItems(function, getSelectedItems());
    MAINWINDOW->openSqlEditor(getModel()->getDb(), sql);
    EditorWindow* win = MAINWINDOW->openSqlEditor(getModel()->getDb(), sql);
    if (!win)
        return;

    static_qstring(tpl, "%1(...)");
    win->getMdiWindow()->rename(tpl.arg(function));
    win->execute();
}

void SqlQueryView::generateInsert()
{
    QString sql = getModel()->generateInsertQueryForItems(getSelectedItems());
    MAINWINDOW->openSqlEditor(getModel()->getDb(), sql);
}

void SqlQueryView::generateUpdate()
{
    QString sql = getModel()->generateUpdateQueryForItems(getSelectedItems());
    MAINWINDOW->openSqlEditor(getModel()->getDb(), sql);
}

void SqlQueryView::generateDelete()
{
    QString sql = getModel()->generateDeleteQueryForItems(getSelectedItems());
    MAINWINDOW->openSqlEditor(getModel()->getDb(), sql);
}

void SqlQueryView::editCurrent()
{
    QModelIndex idx = getCurrentIndex();
    if (idx.isValid())
    {
        SqlQueryItem* item = getModel()->itemFromIndex(idx);
        if (item)
            item->skipInitialFocusSelection();

        if (!simpleBrowserMode)
            edit(idx);
    }
}

void SqlQueryView::toggleRowsHeightAdjustment(bool enabled)
{
    QHeaderView* hdr = verticalHeader();
    if (enabled)
    {
        hdr->resizeSections(QHeaderView::ResizeToContents);
    }
    else
    {
        hdr->setSectionResizeMode(QHeaderView::Interactive);
        hdr->resizeSections(QHeaderView::Interactive);
        int height = hdr->defaultSectionSize();
        int rows = getModel()->rowCount();
        for (int row = 0; row < rows; row++)
            hdr->resizeSection(row, height);
    }
}

void SqlQueryView::toggleNextRowOnEnter(bool enabled)
{
    CFG_UI.General.DataEditAutoAdvanceOnEnter.set(enabled);
}

void SqlQueryView::adjustRowToContents(int section)
{
    verticalHeader()->setSectionResizeMode(section, QHeaderView::ResizeToContents);
}

void SqlQueryView::fontSizeChangeRequested(int delta)
{
    changeFontSize(delta >= 0 ? 1 : -1);
}

void SqlQueryView::incrFontSize()
{
    changeFontSize(1);
}

void SqlQueryView::decrFontSize()
{
    changeFontSize(-1);
}

void SqlQueryView::invertSelection()
{
    SqlQueryModel* model = getModel();
    int rows = model->rowCount();
    int cols = model->columnCount();
    QItemSelectionModel* selection = selectionModel();
    for (int r = 0; r < rows; r++)
        for (int c = 0; c < cols; c++)
            selection->select(model->index(r, c), QItemSelectionModel::Toggle);

    if (!selection->isSelected(currentIndex()))
    {
        QModelIndexList idxList = selection->selectedIndexes();
        if (!idxList.isEmpty())
            selection->setCurrentIndex(selection->selectedIndexes().first(), QItemSelectionModel::NoUpdate);
    }
}

void SqlQueryView::syncPinnedSectionWidth(int logicalIndex, int oldSize, int newSize)
{
    if (pinnedColumns.contains(logicalIndex))
    {
        pinnedView->setColumnWidth(logicalIndex, newSize);
        updatePinnedViewGeometry();
    }
}

void SqlQueryView::syncPinnedSectionHeight(int logicalIndex, int oldSize, int newSize)
{
    pinnedView->setRowHeight(logicalIndex, newSize);
}

void SqlQueryView::refreshColumnDelegates()
{
    for (int& colIdx : customColumnDelegates.keys())
        setItemDelegateForColumn(colIdx, nullptr);

    customColumnDelegates.clear();

    CfgTypedEntry<QHash<QString,QString>>& cfg = CFG_UI.General.DataRenderers;
    auto h = cfg.get();
    StrHash<QString> typeToPlugin = h;
    QList<CellRendererPlugin*> plugins = PLUGINS->getLoadedPlugins<CellRendererPlugin>();
    QHash<QString, CellRendererPlugin*> pluginByName = plugins | TO_HASH(plugin, {return plugin->getName();});

    int i = 0;
    for (const SqlQueryModelColumnPtr& column : getModel()->getColumns())
    {
        int colIdx = i++;
        CellRendererPlugin* plugin = nullptr;

        QString bestId = column->bestEffortIdentifier();
        if (manualCustomDelegates.contains(bestId))
        {
            plugin = manualCustomDelegates.value(bestId);
            if (plugin == nullptr)
            {
                // Manual renderer was set to Default (null),
                // which means type-based renderer is overwritten by Default renderer.
                continue;
            }

            if (!plugins.contains(plugin))
            {
                // Manually selected plugin was unloaded
                manualCustomDelegates.remove(bestId);
                plugin = nullptr;
            }
        }

        if (!plugin)
        {
            QString type = column->dataType.toString();
            if (!typeToPlugin.contains(type, Qt::CaseInsensitive))
                continue;

            QString pluginName = typeToPlugin.value(type, Qt::CaseInsensitive);
            if (pluginName.isEmpty())
                continue;

            if (!pluginByName.contains(pluginName))
            {
                qWarning() << "Type" << type << "is configured to be rendered with plugin" << pluginName << "but such plugin is not loaded.";;
                continue;
            }

            plugin = pluginByName[pluginName];
        }

        QAbstractItemDelegate* delegate = plugin->createDelegate();
        setItemDelegateForColumn(colIdx, delegate);
        customColumnDelegates[colIdx] = plugin;
    }
}

bool SqlQueryView::editInEditorIfNecessary(SqlQueryItem* item)
{
    QList<MultiEditorWidgetPlugin*> plugins = PLUGINS->getLoadedPlugins<MultiEditorWidgetPlugin>();
    for (MultiEditorWidgetPlugin* plugin : plugins)
    {
        int prio = plugin->getPriority(item->getValue(), item->getColumn()->dataType);
        if (prio <= 3)
        {
            openValueEditor(item);
            return false;
        }
    }
    return true;
}

void SqlQueryView::paste(const QList<QList<QVariant> >& data)
{
    if (simpleBrowserMode)
        return;

    QList<SqlQueryItem*> selectedItems = getSelectedItems();
    if (selectedItems.isEmpty())
    {
        notifyWarn(tr("No items selected to paste clipboard contents to."));
        return;
    }

    if (getModel()->isStructureOutOfDate())
    {
        notifyWarn(tr("Cannot paste data. Details: %1").arg(tr("Structure of at least one table used has changed since last data was loaded. Reload the data to proceed.")));
        return;
    }

    QSet<QString> warnedColumns;
    bool warnedRowDeletion = false;
    if (data.size() == 1 && data[0].size() == 1)
    {
        QVariant theValue = data[0][0];
        for (SqlQueryItem* item : selectedItems)
        {
            if (!validatePasting(warnedColumns, warnedRowDeletion, item))
                continue;

            item->setValue(theValue, false);
        }

        return;
    }

    SqlQueryItem* topLeft = selectedItems.first();

    int columnCount = getModel()->columnCount();
    int rowCount = getModel()->rowCount();
    int rowIdx = topLeft->row();
    int colIdx = topLeft->column();

    SqlQueryItem* item = nullptr;

    for (const QList<QVariant>& cells : data)
    {
        // Check if we're out of rows range
        if (rowIdx >= rowCount)
        {
            // No more rows available.
            qDebug() << "Tried to paste more rows than available in the grid.";
            break;
        }

        for (const QVariant& cell : cells)
        {
            // Get current cell
            if (colIdx >= columnCount)
            {
                // No more columns available.
                qDebug() << "Tried to paste more columns than available in the grid.";
                break;
            }
            item = getModel()->itemFromIndex(rowIdx, colIdx++);

            // Set value to the cell, if possible
            if (!validatePasting(warnedColumns, warnedRowDeletion, item))
                continue;

            item->setValue(cell, false);
        }

        // Go to next row, first column
        rowIdx++;
        colIdx = topLeft->column();
    }
}

bool SqlQueryView::validatePasting(QSet<QString>& warnedColumns, bool& warnedRowDeletion, SqlQueryItem* item)
{
    if (item->isDeletedRow())
    {
        if (!warnedRowDeletion)
        {
            warnedRowDeletion = true;
            notifyWarn(tr("Cannot paste to a cell. Details: %1").arg(tr("The row is marked for deletion.")));
        }
        return false;
    }

    if (!item->getColumn()->canEdit())
    {
        QString colName = item->getColumn()->displayName;
        if (!warnedColumns.contains(colName))
        {
            warnedColumns << colName;
            notifyWarn(tr("Cannot paste to column %1. Details: %2").arg(colName, item->getColumn()->getEditionForbiddenReason()));
        }
        return false;
    }

    return true;
}

void SqlQueryView::addFkActionsToContextMenu(SqlQueryItem* currentItem)
{
    QList<SqlQueryModelColumn::ConstraintFk*> fkList = currentItem->getColumn()->getFkConstraints();
    if (fkList.isEmpty())
        return;

    QAction* act;
    if (fkList.size() == 1)
    {
        SqlQueryModelColumn::ConstraintFk* fk = fkList.first();
        act = contextMenu->addAction(tr("Go to referenced row in table '%1'").arg(fk->foreignTable));
        connect(act, &QAction::triggered, [this, fk, currentItem](bool) {
            goToReferencedRow(fk->foreignTable, fk->foreignColumn, currentItem->getValue());
        });
        contextMenu->addSeparator();
        return;
    }

    referencedTablesMenu->clear();
    contextMenu->addMenu(referencedTablesMenu);
    for (SqlQueryModelColumn::ConstraintFk* fk : fkList)
    {
        act = referencedTablesMenu->addAction(tr("table '%1'").arg(fk->foreignTable));
        connect(act, &QAction::triggered, [this, fk, currentItem](bool) {
            goToReferencedRow(fk->foreignTable, fk->foreignColumn, currentItem->getValue());
        });
    }
    contextMenu->addSeparator();
}

void SqlQueryView::goToReferencedRow(const QString& table, const QString& column, const QVariant& value)
{
    Db* db = getModel()->getDb();
    if (!db || !db->isValid())
        return;

    static_qstring(sqlTpl, "SELECT * FROM %1 WHERE %2 = %3");

    QString wrappedTable = wrapObjIfNeeded(table);
    QString wrappedColumn = wrapObjIfNeeded(column);
    QString valueStr = valueToSqlLiteral(value.toString());
    EditorWindow* win = MAINWINDOW->openSqlEditor(db, sqlTpl.arg(wrappedTable, wrappedColumn, valueStr));
    if (!win)
        return;

    win->getMdiWindow()->rename(tr("Referenced row (%1)").arg(table));
    win->execute();
}

void SqlQueryView::copy(bool withHeader, bool cutOperation)
{
    if (simpleBrowserMode)
        return;

    QList<SqlQueryItem*> selectedItems = getSelectedItems();
    QList<QList<SqlQueryItem*> > groupedItems = SqlQueryModel::groupItemsByRows(selectedItems);

    if (selectedItems.isEmpty())
        return;

    QVariant itemValue;
    QStringList cells;
    QList<QStringList> rows;

    QPair<QString,QList<QList<QVariant>>> theDataPair;
    QList<QList<QVariant>> theData;
    QList<QVariant> theDataRow;

    // Header
    if (withHeader)
    {
        QList<SqlQueryModelColumn*> columnModels;
        for (SqlQueryItem*& item : selectedItems)
        {
            SqlQueryModelColumn* itemColModel = getModel()->getColumns()[item->column()].data();
            if (columnModels.contains(itemColModel))
                continue;

            columnModels << itemColModel;
        }

        for (SqlQueryModelColumn*& col : columnModels)
        {
            theDataRow << col->displayName;
            cells << col->displayName;
        }

        rows << cells;
        cells.clear();

        theData << theDataRow;
        theDataRow.clear();
    }

    // Data
    for (const QList<SqlQueryItem*>& itemsInRows : groupedItems)
    {
        for (SqlQueryItem* item : itemsInRows)
        {
            itemValue = item->getValue();
            if (itemValue.userType() == QMetaType::Double)
                cells << doubleToString(itemValue);
            else
                cells << itemValue.toString();

            theDataRow << itemValue;

            if (cutOperation && item->getColumn()->editionForbiddenReason.isEmpty())
                item->setValue(QVariant(QString()), false);
        }

        rows << cells;
        cells.clear();

        theData << theDataRow;
        theDataRow.clear();
    }

    QMimeData* mimeData = new QMimeData();
    QString tsv = TsvSerializer::serialize(rows);
    mimeData->setText(tsv);

    QString md5 = QCryptographicHash::hash(tsv.toUtf8(), QCryptographicHash::Md5);
    theDataPair.first = md5;
    theDataPair.second = theData;

    QByteArray serializedData;
    QDataStream stream(&serializedData, QIODevice::WriteOnly);
    stream << theDataPair;
    mimeData->setData(mimeDataId, serializedData);

    qApp->clipboard()->setMimeData(mimeData);
}

void SqlQueryView::changeFontSize(int factor)
{
    auto f = CFG_UI.Fonts.DataView.get();
    f.setPointSize(f.pointSize() + factor);
    CFG_UI.Fonts.DataView.set(f);
}

void SqlQueryView::headerMiddleClicked(int colIdx)
{
    emit headerMiddleButtonClicked(colIdx);
}

void SqlQueryView::requestColumnSorting(int colIdx)
{
    emit columnSortingRequested(colIdx);
}

void SqlQueryView::setItemDelegateForColumn(int column, QAbstractItemDelegate* delegate)
{
    QTableView::setItemDelegateForColumn(column, delegate);
    pinnedView->setItemDelegateForColumn(column, delegate);
}

void SqlQueryView::moveToNextRowAndEdit()
{
    if (!CFG_UI.General.DataEditAutoAdvanceOnEnter.get())
        return;

    QModelIndex current = currentIndex();
    QModelIndex next = model()->index(current.row() + 1, current.column());
    if (!next.isValid())
        return;

    setCurrentIndex(next);

    if (CFG_UI.General.DataEditAutoAdvanceEdit.get())
        edit(next);
}

void SqlQueryView::updatePinnedViewGeometry()
{
    QHeaderView* header = horizontalHeader();
    int totalWd = 0;
    for (int logical = 0; logical < header->count(); ++logical)
        if (pinnedColumns.contains(logical))
            totalWd += columnWidth(logical);

    pinnedView->setGeometry(
                verticalHeader()->width() + frameWidth(),
                frameWidth(),
                totalWd,
                viewport()->height()+horizontalHeader()->height()
                );
}

QList<int> SqlQueryView::getPinnedColumns() const
{
    return pinnedColumns;
}

void SqlQueryView::updatePinnedViewColumns()
{
    QHeaderView* header = horizontalHeader();
    for (int logical = 0; logical < header->count(); ++logical)
    {
        int currentVisual = header->visualIndex(logical);
        if (currentVisual != logical)
            header->moveSection(currentVisual, logical);
    }

    int pinnedIdx = 0;
    for (int logical = 0; logical < header->count(); ++logical)
    {
        bool isPinned = pinnedColumns.contains(logical);
        if (isPinned)
        {
            header->moveSection(logical, pinnedIdx++);
            pinnedView->setColumnWidth(logical, columnWidth(logical));
        }
        pinnedView->setColumnHidden(logical, !isPinned);
    }

    updatePinnedViewGeometry();
}

void SqlQueryView::updatePinnedViewRowSizes()
{
    for (int idx = 0; idx < model()->rowCount(); ++idx)
        pinnedView->setRowHeight(idx, rowHeight(idx));
}

void SqlQueryView::handlePluginLoaded(Plugin* plugin, PluginType* pluginType)
{
    Q_UNUSED(pluginType)
    CellRendererPlugin* rendererPlugin = dynamic_cast<CellRendererPlugin*>(plugin);
    if (!rendererPlugin)
        return;

    refreshColumnDelegates();
}

void SqlQueryView::handlePluginUnload(Plugin* plugin, PluginType* pluginType)
{
    Q_UNUSED(pluginType)
    CellRendererPlugin* rendererPlugin = dynamic_cast<CellRendererPlugin*>(plugin);
    if (!rendererPlugin)
        return;

    QMutableHashIterator<int, CellRendererPlugin*> it(customColumnDelegates);
    while (it.hasNext())
    {
        it.next();
        int colIdx = it.key();
        if (it.value() == rendererPlugin)
        {
            setItemDelegateForColumn(colIdx, nullptr);
            it.remove();
        }
    }
}

int SqlQueryView::getColumnCustomDelegateWidth(int colIdx)
{
    QSize size = getColumnCustomDelegateCellSize(colIdx);
    if (!size.isEmpty())
        return size.width();
    else
        return -1;
}

int SqlQueryView::getColumnCustomDelegateHeight(int colIdx)
{
    QSize size = getColumnCustomDelegateCellSize(colIdx);
    if (!size.isEmpty())
        return size.height();
    else
        return -1;
}

QSize SqlQueryView::getColumnCustomDelegateCellSize(int colIdx)
{
    if (!customColumnDelegates.contains(colIdx))
        return QSize();

    CellRendererPlugin* plugin = customColumnDelegates[colIdx];
    return plugin->getPreferredCellSize();
}

QVariant SqlQueryView::getCustomDelegatesForSession() const
{
    QHash<QString, QString> bestIdToPluginName;
    QHashIterator<QString, CellRendererPlugin*> it(manualCustomDelegates);
    while (it.hasNext())
    {
        it.next();
        QString pluginName = it.value() ? it.value()->getName() : QString();
        bestIdToPluginName[it.key()] = pluginName;
    }
    return QVariant::fromValue<QHash<QString, QString>>(bestIdToPluginName);
}

void SqlQueryView::restoreCustomDelegatesFromSession(const QVariant& sessionValue)
{
    if (!sessionValue.canConvert<QHash<QString, QString>>())
        return;

    QHash<QString, QString> bestIdToPluginName = sessionValue.value<QHash<QString, QString>>();
    manualCustomDelegates.clear();
    QHash<QString, CellRendererPlugin*> pluginByName = PLUGINS->getLoadedPlugins<CellRendererPlugin>()
            | TO_HASH(p, {return p->getName();});

    QHashIterator<QString, QString> it(bestIdToPluginName);
    while (it.hasNext())
    {
        it.next();

        QString pluginName = it.value();
        if (pluginName.isEmpty())
        {
            manualCustomDelegates[it.key()] = nullptr;
            continue;
        }

        CellRendererPlugin* plugin = pluginByName.value(pluginName, nullptr);
        if (!plugin)
            continue;

        manualCustomDelegates[it.key()] = plugin;
    }
}

bool SqlQueryView::getSimpleBrowserMode() const
{
    return simpleBrowserMode;
}

void SqlQueryView::setSimpleBrowserMode(bool value)
{
    if (value)
        setEditTriggers(QAbstractItemView::NoEditTriggers);
    else
        setEditTriggers(QAbstractItemView::AnyKeyPressed|QAbstractItemView::EditKeyPressed);

    simpleBrowserMode = value;
}

void SqlQueryView::setIgnoreColumnWidthChanges(bool ignore)
{
    ignoreColumnWidthChanges = ignore;
}

QMenu* SqlQueryView::getHeaderContextMenu() const
{
    return headerContextMenu;
}

void SqlQueryView::setReadOnly(bool value)
{
    setSimpleBrowserMode(value);
}

void SqlQueryView::staticInit()
{
    QHash<QString,QString> renderers = CFG_UI.General.DataRenderers.get();

    for (const QString& typeName : DataType::getAllNames())
    {
        if (!renderers.contains(typeName))
            renderers[typeName] = QString();
    }

    for (CellRendererPlugin* plugin : PLUGINS->getLoadedPlugins<CellRendererPlugin>())
    {
        for (const DataType& type : plugin->getPreferredTypes())
        {
            QString typeName = type.toString();
            if (!renderers.contains(typeName))
                renderers[typeName] = plugin->getName();
        }
    }

    CFG_UI.General.DataRenderers.set(renderers);
}

void SqlQueryView::scrollContentsBy(int dx, int dy)
{
    QTableView::scrollContentsBy(dx, dy);
    emit scrolledBy(dx, dy);
}

void SqlQueryView::keyPressEvent(QKeyEvent *e)
{
    // Overriden EditTrigger, so it works consistently across platforms.
    // Default implementation caused problem with Polish characters under Windows (#5364)
    const QString txt = e->text();
    bool shouldOpenEditor = false;
    if (!txt.isEmpty() && txt.at(0).isPrint() && state() != QAbstractItemView::EditingState)
        shouldOpenEditor = true;

    QTableView::keyPressEvent(e);

    if (shouldOpenEditor && state() != QAbstractItemView::EditingState && !simpleBrowserMode)
    {
        QTableView::edit(currentIndex());
        if (state() == QAbstractItemView::EditingState)
            QApplication::sendEvent(focusWidget(), e);
    }
}

void SqlQueryView::resizeEvent(QResizeEvent* event)
{
    QTableView::resizeEvent(event);
    updatePinnedViewGeometry();
}

QModelIndex SqlQueryView::moveCursor(CursorAction cursorAction, Qt::KeyboardModifiers modifiers)
{
    QModelIndex current = QTableView::moveCursor(cursorAction, modifiers);

    if (cursorAction == MoveLeft && current.column() > 0 && visualRect(current).topLeft().x() < pinnedView->columnWidth(0))
    {
        const int newValue = horizontalScrollBar()->value() +
                             visualRect(current).topLeft().x() -
                             pinnedView->columnWidth(0);
        horizontalScrollBar()->setValue(newValue);
    }
    return current;
}

void SqlQueryView::closeEditor(QWidget* editor, QAbstractItemDelegate::EndEditHint hint)
{
    QTableView::closeEditor(editor, hint);
    if (itemDelegate->getLastCommitReason() == SqlQueryItemDelegate::CommitReason::EnterKey)
        moveToNextRowAndEdit();
}

void SqlQueryView::scrollTo(const QModelIndex& index, ScrollHint hint)
{
    if (index.column() >= 0)
        QTableView::scrollTo(index, hint);
}

void SqlQueryView::updateCommitRollbackActions(bool enabled)
{
    actionMap[COMMIT]->setEnabled(enabled);
    actionMap[ROLLBACK]->setEnabled(enabled);
}

void SqlQueryView::customContextMenuRequested(const QPoint& pos)
{
    if (simpleBrowserMode)
        return;

    SqlQueryItem* currentItem = getCurrentItem();
    QList<SqlQueryItem*> selectedItems = getSelectedItems();

    contextMenu->clear();

    setupActionsForMenu(currentItem, selectedItems);
    emit contextMenuRequested(currentItem, selectedItems);

    if (contextMenu->actions().size() == 0)
        return;

    contextMenu->popup(viewport()->mapToGlobal(pos));
}

void SqlQueryView::headerContextMenuRequested(const QPoint& pos)
{
    if (simpleBrowserMode)
        return;

    int logicalIdx = horizontalHeader()->logicalIndexAt(pos);

    headerContextMenu->clear();
    headerContextMenu->addAction(actionMap[SORT_DIALOG]);
    headerContextMenu->addAction(actionMap[RESET_SORTING]);

    QAction* pinAction = new QAction(tr("Pin column"), headerContextMenu);
    pinAction->setCheckable(true);
    pinAction->setChecked(pinnedColumns.contains(logicalIdx));
    connect(pinAction, &QAction::triggered, [this, logicalIdx](bool checked)
    {
        if (checked)
            pinnedColumns += logicalIdx;
        else
            pinnedColumns.removeOne(logicalIdx);

        ::sSort(pinnedColumns);
        updatePinnedViewColumns();
    });
    connect(pinAction, &QAction::triggered, this, &SqlQueryView::pinnedColumnsChanged);
    headerContextMenu->addAction(pinAction);

    if (headerAdditionalActions.size() > 0)
    {
        headerContextMenu->addSeparator();
        for (QAction*& action : headerAdditionalActions)
            headerContextMenu->addAction(action);
    }

    QList<CellRendererPlugin*> rendererPlugins = PLUGINS->getLoadedPlugins<CellRendererPlugin>();
    if (!rendererPlugins.isEmpty() && logicalIdx > -1)
    {
        QMenu* renderersMenu = new QMenu(headerContextMenu);
        renderersMenu->setTitle(tr("Column renderer"));
        headerContextMenu->addSeparator();
        headerContextMenu->addMenu(renderersMenu);

        QActionGroup* actGrp = new QActionGroup(renderersMenu);
        actGrp->setExclusive(true);

        QAction* act = new QAction(tr("Default"), actGrp);
        connect(act, &QAction::triggered, [this, logicalIdx](bool)
        {
            setItemDelegateForColumn(logicalIdx, nullptr);
            customColumnDelegates.remove(logicalIdx);
            manualCustomDelegates[getModel()->getColumns()[logicalIdx]->bestEffortIdentifier()] = nullptr;
        });
        act->setCheckable(true);
        act->setChecked(true); // by default this is the checked one, but it may be overridden in the loop below if there is a custom renderer for the column
        renderersMenu->addAction(act);

        int idx = 0;
        for (CellRendererPlugin* rendererPlugin : rendererPlugins)
        {
            idx++;
            act = new QAction(rendererPlugin->getRendererName(), actGrp);
            act->setCheckable(true);
            act->setChecked(customColumnDelegates.contains(logicalIdx) && customColumnDelegates[logicalIdx] == rendererPlugin);
            connect(act, &QAction::triggered, [this, logicalIdx, rendererPlugin](bool)
            {
                QAbstractItemDelegate* delegate = rendererPlugin->createDelegate();
                setItemDelegateForColumn(logicalIdx, delegate);
                customColumnDelegates[logicalIdx] = rendererPlugin;
                manualCustomDelegates[getModel()->getColumns()[logicalIdx]->bestEffortIdentifier()] = rendererPlugin;
            });
            renderersMenu->addAction(act);
        }
    }

    headerContextMenu->popup(horizontalHeader()->mapToGlobal(pos));
}

void SqlQueryView::openSortDialog()
{
    QStringList columns;
    for (SqlQueryModelColumnPtr& col : getModel()->getColumns())
        columns << col->displayName;

    SortDialog dialog(this);
    dialog.setColumns(columns);
    dialog.setSortOrder(getModel()->getSortOrder());
    if (dialog.exec() != QDialog::Accepted)
        return;

    getModel()->setSortOrder(dialog.getSortOrder());
}

void SqlQueryView::resetSorting()
{
    getModel()->setSortOrder(QueryExecutor::SortList());
}

void SqlQueryView::sortingUpdated(const QueryExecutor::SortList& sortOrder)
{
    actionMap[RESET_SORTING]->setEnabled(sortOrder.size() > 0);
}

void SqlQueryView::updateFont()
{
    QFont f = CFG_UI.Fonts.DataView.get();
    QFontMetrics fm(f);
    verticalHeader()->setDefaultSectionSize(fm.height() + 4);

    int minWd = fm.horizontalAdvance("99999");
    verticalHeader()->setDefaultAlignment(Qt::AlignRight | Qt::AlignVCenter);
    verticalHeader()->setMinimumWidth(minWd);

    if (getModel())
        getModel()->repaintAllItems();

    emit newFontMetricsApplied();
}

void SqlQueryView::executionStarted()
{
    beforeExecutionHorizontalPosition = horizontalScrollBar()->sliderPosition();
    beforeExecutionVerticalPosition = verticalScrollBar()->sliderPosition();
    widgetCover->show();
}

void SqlQueryView::executionEnded()
{
    if (beforeExecutionHorizontalPosition > -1 || beforeExecutionVerticalPosition > -1)
    {
        if (beforeExecutionHorizontalPosition > -1)
            horizontalScrollBar()->setSliderPosition(beforeExecutionHorizontalPosition);

        if (beforeExecutionVerticalPosition > -1)
            verticalScrollBar()->setSliderPosition(beforeExecutionVerticalPosition);

        emit scrolledBy(beforeExecutionHorizontalPosition > -1 ? beforeExecutionHorizontalPosition : 0, beforeExecutionVerticalPosition > -1 ? beforeExecutionVerticalPosition : 0);
    }

    widgetCover->hide();
}

void SqlQueryView::setCurrentRow(int row)
{
    auto idx = model()->index(row, 0);
    setCurrentIndex(idx);
    scrollTo(idx);
}

void SqlQueryView::cut()
{
    copy(false, true);
}

void SqlQueryView::copy()
{
    copy(false, false);
}

void SqlQueryView::copyWithHeader()
{
    copy(true, false);
}

void SqlQueryView::paste()
{
    if (simpleBrowserMode)
        return;

    const QMimeData* mimeData = qApp->clipboard()->mimeData();
    if (mimeData->hasFormat(mimeDataId))
    {
        QString tsv = mimeData->text();
        QString md5 = QCryptographicHash::hash(tsv.toUtf8(), QCryptographicHash::Md5);

        QPair<QString,QList<QList<QVariant>>> theDataPair;
        QByteArray serializedData = mimeData->data(mimeDataId);
        QDataStream stream(&serializedData, QIODevice::ReadOnly);
        stream >> theDataPair;

        if (md5 == theDataPair.first)
        {
            paste(theDataPair.second);
            return;
        }
    }

    QList<QStringList> deserializedRows = TsvSerializer::deserialize(mimeData->text());
    bool trimOnPaste = false;
    bool trimOnPasteAsked = false;
    bool pasteAsNull = false;
    bool pasteAsNullAsked = false;

    QList<QVariant> dataRow;
    QList<QList<QVariant>> dataToPaste;
    for (const QStringList& cells : deserializedRows)
    {
        for (const QString& cell : cells)
        {
            if (!cell.isEmpty() && (cell.front().isSpace() || cell.back().isSpace()) && !trimOnPasteAsked)
            {
                QMessageBox::StandardButton trimChoice;
                trimChoice = QMessageBox::question(this, tr("Trim pasted text?"),
                                               tr("The pasted text contains leading or trailing white space. Trim it automatically?"));
                trimOnPasteAsked = true;
                trimOnPaste = (trimChoice == QMessageBox::Yes);
            }

            if (cell == "NULL" && !pasteAsNullAsked)
            {
                QMessageBox::StandardButton nullChoice;
                nullChoice = QMessageBox::question(this, tr("Paste \"NULL\" as null value?"),
                                               tr("The pasted text contains \"NULL\" literals. Do you want to consider them as NULL values?"));
                pasteAsNullAsked = true;
                pasteAsNull = (nullChoice == QMessageBox::Yes);
            }

            if (cell == "NULL" && pasteAsNull)
                dataRow << QVariant();
            else
                dataRow << (trimOnPaste ? cell.trimmed() : cell);
        }

        dataToPaste << dataRow;
        dataRow.clear();
    }

    paste(dataToPaste);
}

void SqlQueryView::copyAs()
{
    // TODO copyAs()
}

void SqlQueryView::pasteAs()
{
    // TODO pasteAs()
}

void SqlQueryView::setNull()
{
    if (simpleBrowserMode)
        return;

    for (SqlQueryItem* selItem : getSelectedItems())
    {
        if (selItem->getColumn()->editionForbiddenReason.size() > 0)
            continue;

        selItem->setValue(QVariant(QString()), false);
    }
}

void SqlQueryView::erase()
{
    if (simpleBrowserMode)
        return;

    for (SqlQueryItem* selItem : getSelectedItems())
    {
        if (selItem->getColumn()->editionForbiddenReason.size() > 0)
            continue;

        selItem->setValue("", false);
    }
}

void SqlQueryView::resetValue()
{
    if (simpleBrowserMode)
        return;

    for (SqlQueryItem* selItem : getSelectedItems())
    {
        if (selItem->getColumn()->editionForbiddenReason.size() > 0)
            continue;

        if (!selItem->isNewRow() || !selItem->getColumn()->hasDefaultValueForInsert())
            continue;

        selItem->setValue(QVariant(QString()), false);
        selItem->setUntouched(true);
    }
}

void SqlQueryView::commit()
{
    if (simpleBrowserMode)
        return;

    getModel()->commit();
}

void SqlQueryView::rollback()
{
    if (simpleBrowserMode)
        return;

    getModel()->rollback();
}

void SqlQueryView::selectiveCommit()
{
    if (simpleBrowserMode)
        return;

    getModel()->commit(getSelectedItems());
}

void SqlQueryView::selectiveRollback()
{
    if (simpleBrowserMode)
        return;

    getModel()->rollback(getSelectedItems());
}

void SqlQueryView::openValueEditor(SqlQueryItem* item)
{
    if (simpleBrowserMode)
        return;

    if (!item)
    {
        qWarning() << "Tried to open value editor while there's no current item. It should not be called in that case.";
        return;
    }

    SqlQueryModelColumn* column = item->getColumn();

    MultiEditorDialog editor(this);
    editor.configureResetButton(column);
    if (!column->getFkConstraints().isEmpty())
        editor.enableFk(getModel()->getDb(), column);

    editor.setWindowTitle(tr("Edit value"));
    editor.setValue(item->getValue(), column->dataType);
    editor.setReadOnly(!column->canEdit());
    editor.setNewRow(item->isNewRow());
    editor.setUntouched(item->isUntouched());

    if (editor.exec() == QDialog::Rejected)
        return;

    item->setValue(editor.getValue());
    item->setUntouched(editor.isUntouched());
}

void SqlQueryView::openValueEditor()
{
    SqlQueryItem* currentItem = getCurrentItem();
    openValueEditor(currentItem);
}

size_t qHash(SqlQueryView::Action action)
{
    return static_cast<size_t>(action);
}

SqlQueryView::Header::Header(SqlQueryView* parent) :
    QHeaderView(Qt::Horizontal, parent)
{
    connect(this, &QHeaderView::sectionResized, this, &SqlQueryView::Header::handleSectionResize);
}

QSize SqlQueryView::Header::sectionSizeFromContents(int section) const
{
    QSize originalSize = QHeaderView::sectionSizeFromContents(section);
    int colCount = dynamic_cast<SqlQueryView*>(parent())->getModel()->columnCount();
    if (colCount <= 5 || CFG_UI.General.ColumnWidthForName.get())
        return originalSize;

    int wd = minHeaderWidth;
    wd = qMin((wd + wd * 20 / colCount), originalSize.width());
    return QSize(wd, originalSize.height());
}

void SqlQueryView::Header::mousePressEvent(QMouseEvent *e)
{
    if (e->button() == Qt::MiddleButton)
    {
        int section = logicalIndexAt(e->pos());
        if (section >= 0)
        {
            qobject_cast<SqlQueryView*>(parentWidget())->headerMiddleClicked(section);
            e->accept();
            return;
        }
    }

    // Expanded separator tolerance for both hover and press/grab
    if (e->button() == Qt::LeftButton)
    {
        lastSectionSizes.clear();
        int sectionCount = count();
        for (int i = 0; i < sectionCount; i++)
            lastSectionSizes[i] = sectionSize(i);

        int x = e->pos().x();

        // Find nearest section boundary (right edge of section i)
        int foundBoundary = -1;
        int boundaryX = 0;
        for (int i = 0; i < sectionCount; ++i)
        {
            int b = sectionPosition(i) + sectionSize(i);
            if (qAbs(x - b) <= separatorTolerance)
            {
                foundBoundary = i;
                boundaryX = b;
                break;
            }
        }

        if (foundBoundary >= 0)
        {
            // sectionPosition()+sectionSize() points to the first pixel after the section.
            // Click 1 px inside the left section so the last boundary is still resizable.
            int synthX = qBound(0, boundaryX - 1, width() - 1);
            QPoint localSynth(synthX, e->pos().y());
            QPoint globalSynth = mapToGlobal(localSynth);
            QMouseEvent synthetic(QEvent::MouseButtonPress, localSynth, globalSynth, Qt::LeftButton, e->buttons(), e->modifiers());
            QHeaderView::mousePressEvent(&synthetic);
            e->accept();
            return;
        }

        int section = logicalIndexAt(e->pos());
        if (section >= 0 && CFG_UI.General.SingleColumnClickSort.get() && !e->modifiers().testFlag(Qt::AltModifier))
        {
            qobject_cast<SqlQueryView*>(parentWidget())->requestColumnSorting(section);
            return;
        }
    }

    QHeaderView::mousePressEvent(e);
}

void SqlQueryView::Header::mouseDoubleClickEvent(QMouseEvent* e)
{
    QScopedValueRollback scopedRollback(dblClickResizing, true);

    if (e->button() == Qt::LeftButton)
    {
        int x = e->pos().x();
        int sectionCount = count();
        // Find nearest section boundary (right edge of section i)
        int foundBoundary = -1;
        for (int i = 0; i < sectionCount; ++i)
        {
            int b = sectionPosition(i) + sectionSize(i);
            if (qAbs(x - b) <= separatorTolerance)
            {
                foundBoundary = i;
                break;
            }
        }

        if (foundBoundary >= 0)
        {
            auto queryView = qobject_cast<SqlQueryView*>(parentWidget());

            QModelIndexList selColIdxList = selectionModel()->selectedColumns();
            if (!selColIdxList.isEmpty())
            {
                // #5756 If multiple columns are selected, resize all of them to contents
                // Qt does not handle it by default, if the clicked boundry section doesn't require resizing itself.
                // We need to enforce it for all selected columns.
                for (auto&& idx : selColIdxList)
                    queryView->resizeColumnToContents(idx.column());
            }
            else
                queryView->resizeColumnToContents(foundBoundary);

            e->accept();
            return;
        }

        int section = logicalIndexAt(e->pos());
        if (section >= 0)
        {
            // Double-clicked on the section - sort the column if configured to do so
            if (CFG_UI.General.SingleColumnClickSort.get())
                e->accept();
            else
                qobject_cast<SqlQueryView*>(parentWidget())->requestColumnSorting(section);
        }

        return;
    }
    QHeaderView::mouseDoubleClickEvent(e);
}

void SqlQueryView::Header::mouseMoveEvent(QMouseEvent *e)
{
    // Only change cursor on hover (no drag buttons pressed)
    if (!(e->buttons() & Qt::LeftButton))
    {
        int x = e->pos().x();
        int sectionCount = count();
        bool nearBoundary = false;

        for (int i = 0; i < sectionCount; ++i)
        {
            int b = sectionPosition(i) + sectionSize(i);
            if (qAbs(x - b) <= separatorTolerance)
            {
                nearBoundary = true;
                break;
            }
        }

        if (nearBoundary)
            setCursor(Qt::SplitHCursor);
        else
            unsetCursor();

        e->accept();
        return;
    }

    QHeaderView::mouseMoveEvent(e);
}

void SqlQueryView::Header::leaveEvent(QEvent* e)
{
    unsetCursor();
    QHeaderView::leaveEvent(e);
}

void SqlQueryView::Header::handleSectionResize(int logicalIndex, int oldSize, int newSize)
{
    Q_UNUSED(oldSize);
    Q_UNUSED(newSize);

    if (ignoreResizing)
        return;

    QScopedValueRollback scopedIgnore(ignoreResizing, true);

    SqlQueryView* view = qobject_cast<SqlQueryView*>(parentWidget());
    QList<int> cols = view->selectionModel()->selectedColumns() | MAP(idx, {return idx.column();});
    if (cols.size() <= 1 || !cols.contains(logicalIndex))
        return;

    if (dblClickResizing)
    {
        for (int col : cols)
        {
            if (col == logicalIndex)
                continue;

            view->resizeColumnToContents(col);
        }
    }
}
