/*
 * Copyright (C) 2014-2024 CZ.NIC
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * In addition, as a special exception, the copyright holders give
 * permission to link the code of portions of this program with the
 * OpenSSL library under certain conditions as described in each
 * individual source file, and distribute linked combinations including
 * the two.
 */

#include <algorithm> /* ::std::sort */
#include <QMenuBar>
#include <QSet>
#include <QToolBar>

#include "src/datovka_shared/compat/compiler.h" /* ignoreRetVal */
#include "src/datovka_shared/log/log.h"
#include "src/datovka_shared/settings/prefs.h"
#include "src/dimensions/dimensions.h"
#include "src/global.h"
#include "src/gui/dlg_tool_bar.h"
#include "src/gui/icon_container.h"
#include "src/json/action_entry.h"
#include "src/model_interaction/view_interaction.h"
#include "src/models/sort_filter_proxy_model.h"
#include "src/settings/prefs_specific.h"
#include "src/views/table_key_press_filter.h"
#include "src/views/table_space_selection_filter.h"
#include "ui_dlg_tool_bar.h"

#define MAIN_TOP_TOOLBAR_STYLE_KEY "window.main.toolbar.button.style"
#define MAIN_TOP_TOOLBAR_CONTENT_KEY "window.main.toolbar.content"

#define VAL_ALL -1

/*!
 * @brief Convert menu actions into model entries.
 *
 * @param[in] menu Menu bar for actions.
 * @return Menu action list.
 */
static
QList<ActionListModel::Entry> constructEntriesFromMenu(const QMenu *menu)
{
	QList<ActionListModel::Entry> entryList;

	if (Q_UNLIKELY(menu == Q_NULLPTR)) {
		Q_ASSERT(0);
		return entryList;
	}

	for (QAction *action : menu->actions()) {
		if (Q_UNLIKELY(action == Q_NULLPTR)) {
			continue;
		}
		if (action->isSeparator()) {
			continue;
		}
		{
			QMenu *menu = action->menu();
			if (menu != Q_NULLPTR) {
				entryList.append(constructEntriesFromMenu(menu));
				continue;
			}
		}
		{
			entryList.append(ActionListModel::Entry(
			    ActionListModel::Entry::TYPE_ACTION, false, action));
		}
	}

	return entryList;
}

/*!
 * @brief Load stored action list from preferences.
 *
 * @param[in] ok True on success.
 * @return Stored action list.
 */
static
Json::ActionEntryList loadStoredList(bool *ok = Q_NULLPTR)
{
	bool iOk = false;
	Json::ActionEntryList storedList;

	QString val;
	if (Q_UNLIKELY(!GlobInstcs::prefsPtr->strVal(MAIN_TOP_TOOLBAR_CONTENT_KEY, val))) {
		goto fail;
	}
	if (val.isEmpty()) {
		goto fail;
	}

	iOk = false;
	storedList = Json::ActionEntryList::fromJson(val.toUtf8(), &iOk);
	if (Q_UNLIKELY(!iOk)) {
		goto fail;
	}

fail:
	if (ok != Q_NULLPTR) {
		*ok = iOk;
	}
	return storedList;
}

/*!
 * @brief Construct model entries from tool bar.
 *
 * @param[in] toolBar Tool bar to scan for actions.
 * @param[in] firstUnconfigurableToolbarElement First init action.
 * @return List of model action entries.
 */
static
QList<ActionListModel::Entry> constructEntriesFromToolBar(const QToolBar *toolBar,
    QAction *firstUnconfigurableToolbarElement)
{
	QList<ActionListModel::Entry> entryList;

	if (Q_UNLIKELY(toolBar == Q_NULLPTR)) {
		Q_ASSERT(0);
		return entryList;
	}

	for (QAction *action : toolBar->actions()) {
		if (Q_UNLIKELY(action == Q_NULLPTR)) {
			continue;
		}
		if (action == firstUnconfigurableToolbarElement) {
			/* Non-null and equal. */
			break;
		}

		entryList.append(ActionListModel::Entry(
		    ActionListModel::Entry::TYPE_ACTION, true, action));
	}

	return entryList;
}

/*!
 * @brief Construct model entries from stored entries and available entries.
 *
 * @note May contain unchecked values.
 *
 * @param[in] availableMenus List of available menus, containing actions.
 * @param[in] toolBar Tool bar to scan for actions.
 * @param[in] firstUnconfigurableToolbarElement Tool bar action before which to stop.
 * @return List of model action entries.
 */
static
QList<ActionListModel::Entry> constructStoredEntries(
    const QList<QMenu *> &availableMenus, const QToolBar *toolBar,
    QAction *firstUnconfigurableToolbarElement)
{
	QList<ActionListModel::Entry> availableEntries;
	QList<ActionListModel::Entry> toolBarEntries =
	    constructEntriesFromToolBar(toolBar, firstUnconfigurableToolbarElement);
	QList<ActionListModel::Entry> constructedEntryList;
	{
		for (const QMenu *menu : availableMenus) {
			availableEntries.append(constructEntriesFromMenu(menu));
		}
	}
	bool iOk = false;
	Json::ActionEntryList storedList = loadStoredList(&iOk);
	if (Q_UNLIKELY(!iOk)) {
		goto fail;
	}

	if (Q_UNLIKELY(storedList.size() < toolBarEntries.size())) {
		/*
		 * Tool bar content should be already created.
		 * Stored list must be the same length as the tool-bar list or longer.
		 */
		goto fail;
	}

	{
		int iToolBar = 0;
		for (const Json::ActionEntry &storedEntry : storedList) {
			switch (storedEntry.type()) {
			case Json::ActionEntry::TYPE_ACTION:
				if (storedEntry.checked()) {
					if (Q_UNLIKELY(iToolBar >= toolBarEntries.size())) {
						logErrorNL("%s", "Already at the end of tool bar entries.");
						goto fail;
					}
					{
						const ActionListModel::Entry &toolBarEntry = toolBarEntries.at(iToolBar);
						QAction *action = qobject_cast<QAction *>(toolBarEntry.controlObject);
						if (Q_UNLIKELY(action == Q_NULLPTR)) {
							goto fail;
						}
						if (Q_UNLIKELY(action->objectName() != storedEntry.objectName())) {
							goto fail;
						}
						/* Both entries actions with matching names. */
						constructedEntryList.append(toolBarEntry);
						++iToolBar;
					}
				} else {
					/* Find respective action. */
					ActionListModel::Entry froundEntry;
					for (const ActionListModel::Entry &availableEntry : availableEntries) {
						if (availableEntry.type == ActionListModel::Entry::TYPE_ACTION) {
							QAction *action = qobject_cast<QAction *>(availableEntry.controlObject);
							if ((action != Q_NULLPTR) &&
							    (!action->isSeparator()) &&
							    (action->objectName() == storedEntry.objectName())) {
								constructedEntryList.append(ActionListModel::Entry(availableEntry.type, false, availableEntry.controlObject));
							}
						}
					}
				}
				break;
			case Json::ActionEntry::TYPE_SEPARATOR:
				if (Q_UNLIKELY(iToolBar >= toolBarEntries.size())) {
					logErrorNL("%s", "Already at the end of tool bar entries.");
					goto fail;
				}
				{
					const ActionListModel::Entry &toolBarEntry = toolBarEntries.at(iToolBar);
					QAction *action = qobject_cast<QAction *>(toolBarEntry.controlObject);
					if (Q_UNLIKELY(action == Q_NULLPTR)) {
						goto fail;
					}
					if (Q_UNLIKELY(!action->isSeparator())) {
						goto fail;
					}
					/* Both entries are separators. */
					constructedEntryList.append(toolBarEntry);
					++iToolBar;
				}
				break;
			default:
				logErrorNL("Cannot handle stored type %d.", storedEntry.type());
				goto fail;
				break;
			}
		}
	}

	return constructedEntryList;

fail:
	return toolBarEntries;
}

/*!
 * @brief Construct actions from tool bar.
 *
 * @param[in] toolBar Tool bar to scan for actions.
 * @param[in] firstUnconfigurableToolbarElement First init action.
 * @return List of actions.
 */
static
QList<QAction *> conctructActions(const QToolBar *toolBar,
    QAction *firstUnconfigurableToolbarElement)
{
	QList<QAction *> actionList;

	if (Q_UNLIKELY(toolBar == Q_NULLPTR)) {
		Q_ASSERT(0);
		return actionList;
	}

	for (QAction *action : toolBar->actions()) {
		if (Q_UNLIKELY(action == Q_NULLPTR)) {
			continue;
		}
		if (action == firstUnconfigurableToolbarElement) {
			/* Non-null and equal. */
			break;
		}

		actionList.append((!action->isSeparator()) ? action : Q_NULLPTR);
	}

	return actionList;
}

DlgToolBar::DlgToolBar(QMenuBar *menuBar,  QToolBar *toolBar,
    QAction *firstUnconfigurableToolbarElement,
    const QList<QAction *> &defaultToolBarActions, QWidget *parent)
    : QDialog(parent),
    m_ui(new (::std::nothrow) Ui::DlgToolBar),
    m_menuBar(menuBar),
    m_toolBar(toolBar),
    m_firstUnconfigurableToolbarElement(firstUnconfigurableToolbarElement),
    m_defaultToolBarActions(defaultToolBarActions),
    m_availableMenuModel(this),
    m_availableMenus(),
    m_availableOperationsProxyModel(this),
    m_dfltFilerLineStyleSheet(SortFilterProxyModel::invalidFilterEditStyle),
    m_availableOperationsModel(this),
    m_assignedOperationsModel(this)
{
	m_ui->setupUi(this);

	initDlg();

	/* Read menu bar menus and actions. */
	if (m_menuBar != Q_NULLPTR) {
		m_availableMenuModel.appendRow(tr("All commands"), VAL_ALL, true);

		for (QAction *action : m_menuBar->actions()) {
			if (Q_UNLIKELY(action == Q_NULLPTR)) {
				continue;
			}
			if (action->isSeparator()) {
				continue;
			}
			{
				QMenu *menu = action->menu();
				if (menu != Q_NULLPTR) {
					m_availableMenuModel.appendRow(
					    menu->title(),
					    m_availableMenus.size(), true);
					m_availableMenus.append(menu);
				}
			}
		}
	}

	/* Load operations. */
	loadAvailableOperations(m_ui->categoryComboBox->currentIndex());

	/* Load button style. */
	{
		qint64 val = PrefsSpecific::TEXT_UNDER_ICON;
		if (GlobInstcs::prefsPtr->intVal(MAIN_TOP_TOOLBAR_STYLE_KEY, val)) {
			switch (val) {
			case PrefsSpecific::ICON_ONLY:
				m_ui->iconOnlyRadio->setChecked(true);
				break;
			case PrefsSpecific::TEXT_BESIDE_ICON:
				m_ui->textBesideIconRadio->setChecked(true);
				break;
			case PrefsSpecific::TEXT_UNDER_ICON:
				m_ui->textUnderIconRadio->setChecked(true);
				break;
			default:
				Q_ASSERT(0);
				break;
			}
		}
	}

	if (m_toolBar != Q_NULLPTR) {
		m_assignedOperationsModel.insertActions(
		    constructStoredEntries(m_availableMenus, m_toolBar,
		        m_firstUnconfigurableToolbarElement));
	}
}

DlgToolBar::~DlgToolBar(void)
{
	delete m_ui;
}

/*!
 * @brief Convert model into JSON type.
 *
 * @param[in] modelType Model type.
 * @return JSON type.
 */
static
enum Json::ActionEntry::Type actionTypeModel2Json(
    enum ActionListModel::Entry::Type modelType)
{
	switch (modelType) {
	case ActionListModel::Entry::TYPE_UNKNOWN:
		return Json::ActionEntry::TYPE_UNKNOWN;
		break;
	case ActionListModel::Entry::TYPE_ACTION:
		return Json::ActionEntry::TYPE_ACTION;
		break;
	case ActionListModel::Entry::TYPE_WIDGET:
		return Json::ActionEntry::TYPE_WIDGET;
		break;
	default:
		return Json::ActionEntry::TYPE_UNKNOWN;
		break;
	}
}

/*!
 * @brief Convert model content into JSON action list.
 *
 * @param[in] modelEntries Assigned model content.
 * @return JSON action list.
 */
static
Json::ActionEntryList actionEntryListModel2Json(
    const QList<ActionListModel::Entry> &modelEntries)
{
	Json::ActionEntryList jsonEntries;

	for (const ActionListModel::Entry &modelEntry : modelEntries) {
		Json::ActionEntry jsonEntry;

		if (modelEntry.type == ActionListModel::Entry::TYPE_ACTION) {
			QAction *action = qobject_cast<QAction *>(modelEntry.controlObject);
			if (Q_UNLIKELY(action == Q_NULLPTR)) {
				Q_ASSERT(0);
				return Json::ActionEntryList();
			}
			if (!action->isSeparator()) {
				jsonEntry.setType(actionTypeModel2Json(modelEntry.type));
				jsonEntry.setChecked(modelEntry.checked);
				jsonEntry.setObjectName(action->objectName());
			} else {
				jsonEntry.setType(Json::ActionEntry::TYPE_SEPARATOR);
				jsonEntry.setChecked(true);
			}
		}

		jsonEntries.append(jsonEntry);
	}

	return jsonEntries;
}

/*!
 * @brief Convert action list into JSON action list.
 *
 * @param[in] actions List of actions.
 * @return JSON action list.
 */
static
Json::ActionEntryList actionList2Json(const QList<QAction *> actions)
{
	Json::ActionEntryList jsonEntries;

	for (QAction *action : actions) {
		if (action != Q_NULLPTR) {
			jsonEntries.append(Json::ActionEntry(Json::ActionEntry::TYPE_ACTION, true, action->objectName()));
		} else {
			jsonEntries.append(Json::ActionEntry(Json::ActionEntry::TYPE_SEPARATOR, true, QString()));
		}
	}

	return jsonEntries;
}

/*!
 * @brief Remove actions from tool bar before given action.
 *
 * @param[in] toolBar Tool bar to to remove actions from.
 * @param[in] befire Action before which to remove.
 */
static
void removeToolBarActions(QToolBar *toolBar, QAction *before = Q_NULLPTR)
{
	if (Q_UNLIKELY(toolBar == Q_NULLPTR)) {
		Q_ASSERT(0);
		return;
	}

	for (QAction *action : toolBar->actions()) {
		if (action != before) {
			toolBar->removeAction(action);
		} else {
			return;
		}
	}
}

/*!
 * @brief Converts style to to Qt::ToolButtonStyle value.
 *
 * @param[in] style Style value as used in preferences.
 * @return Value corresponding with enum Qt::ToolButtonStyle.
 */
static
Qt::ToolButtonStyle qtToolButtonStyle(int style)
{
	switch (style) {
	case PrefsSpecific::ICON_ONLY:
		return Qt::ToolButtonIconOnly;
		break;
	case PrefsSpecific::TEXT_BESIDE_ICON:
		return Qt::ToolButtonTextBesideIcon;
		break;
	case PrefsSpecific::TEXT_UNDER_ICON:
		return Qt::ToolButtonTextUnderIcon;
		break;
	default:
		Q_ASSERT(0);
		return Qt::ToolButtonTextUnderIcon; /* Default. */
		break;
	}
}

bool DlgToolBar::configureToolBar(QMenuBar *menuBar,  QToolBar *toolBar,
    QAction *firstUnconfigurableToolbarElement,
    const QList<QAction *> &defaultToolBarActions, QWidget *parent)
{
	if (Q_UNLIKELY(menuBar == Q_NULLPTR)) {
		Q_ASSERT(0);
		return false;
	}

	QList<QAction *> startToolBarActions;
	if (toolBar != Q_NULLPTR) {
		startToolBarActions = conctructActions(toolBar,
		    firstUnconfigurableToolbarElement);
	}
	enum PrefsSpecific::ToolbarButtonStyle startButtonStyle =
	    (enum PrefsSpecific::ToolbarButtonStyle)toolBarButtonStyle();

	DlgToolBar dlg(menuBar, toolBar, firstUnconfigurableToolbarElement,
	    defaultToolBarActions, parent);

	const QString dlgName("tool_bar");
	const QSize dfltSize = dlg.size();
	{
		const QSize newSize = Dimensions::dialogueSize(&dlg,
		    PrefsSpecific::dlgSize(*GlobInstcs::prefsPtr, dlgName),
		    dfltSize);
		if (newSize.isValid()) {
		    dlg.resize(newSize);
		}
	}

	const int ret = dlg.exec();

	PrefsSpecific::setDlgSize(*GlobInstcs::prefsPtr, dlgName,
	    dlg.size(), dfltSize);

	if (QDialog::Accepted == ret) {
		/* Save content settings. */
		const Json::ActionEntryList defaultList =
		    actionList2Json(defaultToolBarActions);
		const Json::ActionEntryList actualList =
		    actionEntryListModel2Json(dlg.m_assignedOperationsModel.entries());

		if (defaultList != actualList) {
			GlobInstcs::prefsPtr->setStrVal(MAIN_TOP_TOOLBAR_CONTENT_KEY,
			    actualList.toJsonData());
		} else {
			/* Erase entry. */
			GlobInstcs::prefsPtr->resetVal(MAIN_TOP_TOOLBAR_CONTENT_KEY);
		}

		/* Save button style. */
		{
			qint64 val = PrefsSpecific::TEXT_UNDER_ICON;
			if (dlg.m_ui->iconOnlyRadio->isChecked()) {
				val = PrefsSpecific::ICON_ONLY;
			} else if (dlg.m_ui->textBesideIconRadio->isChecked()) {
				val = PrefsSpecific::TEXT_BESIDE_ICON;
			} else if (dlg.m_ui->textUnderIconRadio->isChecked()) {
				val = PrefsSpecific::TEXT_UNDER_ICON;
			}
			GlobInstcs::prefsPtr->setIntVal(MAIN_TOP_TOOLBAR_STYLE_KEY, val);
		}
	} else {
		/* Restore content at start. */
		if (toolBar != Q_NULLPTR) {
			removeToolBarActions(toolBar,
			    firstUnconfigurableToolbarElement);

			insertToolBarActions(toolBar, startToolBarActions,
			    firstUnconfigurableToolbarElement);

			toolBar->setToolButtonStyle(
			    qtToolButtonStyle(startButtonStyle));
		}
	}

	return true;
}

void DlgToolBar::filterFound(const QString &text)
{
	/* Store style at first invocation. */
	if (m_dfltFilerLineStyleSheet == SortFilterProxyModel::invalidFilterEditStyle) {
		m_dfltFilerLineStyleSheet = m_ui->filterLine->styleSheet();
	}

#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0))
	m_availableOperationsProxyModel.setFilterRegularExpression(
	    QRegularExpression(QRegularExpression::escape(text),
	        QRegularExpression::CaseInsensitiveOption));
#else /* < Qt-5.15 */
	m_availableOperationsProxyModel.setFilterRegExp(QRegExp(text,
	    Qt::CaseInsensitive, QRegExp::FixedString));
#endif /* >= Qt-5.15 */
	/* Set filter field background colour. */
	if (text.isEmpty()) {
		m_ui->filterLine->setStyleSheet(m_dfltFilerLineStyleSheet);
	} else if (m_availableOperationsProxyModel.rowCount() != 0) {
		m_ui->filterLine->setStyleSheet(
		    SortFilterProxyModel::foundFilterEditStyle);
	} else {
		m_ui->filterLine->setStyleSheet(
		    SortFilterProxyModel::notFoundFilterEditStyle);
	}
}

void DlgToolBar::loadAvailableOperations(int availableMenuIndex)
{
	const int value = m_ui->categoryComboBox->itemData(availableMenuIndex,
	    CBoxModel::ROLE_VALUE).toInt();

	m_availableOperationsModel.removeAllRows();

	if (value == VAL_ALL) {
		for (const QMenu *menu : m_availableMenus) {
			m_availableOperationsModel.insertActions(
			    constructEntriesFromMenu(menu));
		}
	} else {
		if (value >= m_availableMenus.size()) {
			Q_ASSERT(0);
			return;
		}
		m_availableOperationsModel.insertActions(
		    constructEntriesFromMenu(m_availableMenus.at(value)));
	}

	m_availableOperationsProxyModel.sort(ActionListModel::COL_ACTION_NAME,
	    Qt::AscendingOrder);

	filterFound(m_ui->filterLine->text());
}

void DlgToolBar::handleAvailableSelectionChange(void)
{
	QList<int> rowList = ViewInteraction::selectedSrcRowNums(
	    *(m_ui->availableOperationsView), m_availableOperationsProxyModel,
	    ActionListModel::COL_ACTION_NAME);

	if (rowList.size() == 1) {
		ActionListModel::Entry entry =
		    m_availableOperationsModel.entryAt(rowList.at(0));

		QAction *action = qobject_cast<QAction *>(entry.controlObject);
		if (Q_UNLIKELY(action == Q_NULLPTR)) {
			Q_ASSERT(0);
			m_ui->descriptionText->clear();
			return;
		}

		m_ui->descriptionText->document()->setPlainText(
		    tr("Label: %1\nCommand: %2\nTooltip: %3")
		        .arg(action->text())
		        .arg(action->objectName())
		        .arg(action->toolTip()));
	} else {
		/* Clear description if multiple entries selected. */
		m_ui->descriptionText->clear();
	}

	m_ui->assignButton->setEnabled(!rowList.isEmpty());
}

void DlgToolBar::handleAssignedSelectionChange(void)
{
	QModelIndexList slctIdxs =
	    m_ui->assignedOperationsView->selectionModel()->selectedRows(
	        ActionListModel::COL_ACTION_NAME);

	m_ui->removeButton->setEnabled(!slctIdxs.isEmpty());

	m_ui->upButton->setEnabled(!slctIdxs.isEmpty());
	m_ui->downButton->setEnabled(!slctIdxs.isEmpty());
}

/*!
 * @brief Get the last selected row index.
 *
 * @param[in] listView List view.
 * @return Index of the last selected row,
 *     -1 if no row selected or the last tow is selected.
 */
static
int lastSelectedRow(QListView *listView, QAbstractItemModel *model)
{
	int lastRow = -1;

	if (Q_UNLIKELY((listView == Q_NULLPTR) && (model == Q_NULLPTR))) {
		return lastRow;
	}

	QModelIndexList slctIdxs = listView->selectionModel()->selectedRows(
	    ActionListModel::COL_ACTION_NAME);
	for (const QModelIndex &index : slctIdxs) {
		if (index.isValid() && (lastRow < index.row())) {
			lastRow = index.row();
		}
	}

	if ((lastRow != -1) && ((lastRow + 1) == model->rowCount())) {
		lastRow = -1; /* Actually the last model row. */
	}

	return lastRow;
}

/*!
 * @brief Insert action to a tool bar.
 *
 * @param[in,out] toolBar Tool bar.
 * @param[in]     before Action before which to insert the new action.
 * @param[in]     action Action to be added.
 * @param[in]     focusPolicy Focus policy.
 * @return True if action has been added and policy set.
 */
static
bool insertToolBarAction(QToolBar *toolBar, QAction *before, QAction *action,
    enum Qt::FocusPolicy focusPolicy = Qt::StrongFocus)
{
	if (Q_UNLIKELY((toolBar == Q_NULLPTR) || (action == Q_NULLPTR))) {
		Q_ASSERT(0);
		return false;
	}

	/* Same action cannot be inserted multiple times. */

	toolBar->insertAction(before, action);

	if (focusPolicy == Qt::NoFocus) {
		return true;
	}

	QWidget *w = toolBar->widgetForAction(action);
	if (Q_UNLIKELY(w == Q_NULLPTR)) {
		return false;
	}
	w->setFocusPolicy(focusPolicy);
	return true;
}

/*!
 * @brief Get control object pointers from model data.
 *
 * @param[in] entries List of model entries.
 * @return Set of control object pointers.
 */
static
QSet<QObject *> controlObjects(const QList<ActionListModel::Entry> &entries)
{
	QSet<QObject *> controlObjectSet;
	for (const ActionListModel::Entry &entry : entries) {
		controlObjectSet.insert(entry.controlObject);
	}
	return controlObjectSet;
}

/*!
 * @brief Find first checked action past given position.
 *
 * @param[in] pos Position to start at.
 * @param[in] assignedOperationsModel Model to search in.
 * @param[in] dflt Default value to return.
 * @return Found non-null checked action or \a delft if no checked action found
 *     or if \a pos out of bounds.
 */
static
QAction *activeAssignedActionFromPosition(int pos,
    const ActionListModel &assignedOperationsModel, QAction *dflt = Q_NULLPTR)
{
	if (Q_UNLIKELY(pos < 0)) {
		return dflt;
	}

	if (pos < assignedOperationsModel.rowCount()) {
		for (int row = pos; row < assignedOperationsModel.rowCount(); ++row) {
			const ActionListModel::Entry entry =
			    assignedOperationsModel.entryAt(row);
			QAction *action = qobject_cast<QAction *>(entry.controlObject);
			if (Q_UNLIKELY((!entry.checked) || (action == Q_NULLPTR))) {
				continue;
			}
			return action;
		}
	}

	return dflt;
}

void DlgToolBar::assignSelectedAvailableOperations(void)
{
	QList<int> rowList = ViewInteraction::selectedSrcRowNums(
	    *(m_ui->availableOperationsView), m_availableOperationsProxyModel,
	    ActionListModel::COL_ACTION_NAME);

	QList<ActionListModel::Entry> entries;

	{
		const QSet<QObject *> assignedControlObjects =
		    controlObjects(m_assignedOperationsModel.entries());

		for (int row : rowList) {
			ActionListModel::Entry entry =
			    m_availableOperationsModel.entryAt(row);
			entry.checked = true;

			/* Don't insert already assigned operations. */
			if (Q_UNLIKELY(assignedControlObjects.contains(entry.controlObject))) {
				continue;
			}

			entries.append(entry);
		}
	}

	if (Q_UNLIKELY(entries.isEmpty())) {
		return;
	}

	/* Find last selected entry. */
	int lastRow = lastSelectedRow(m_ui->assignedOperationsView,
	    &m_assignedOperationsModel);

	if (m_toolBar != Q_NULLPTR) {
		/* Find position in tool bar. */
		QAction *before = m_firstUnconfigurableToolbarElement;
		if (lastRow != -1) {
			before = activeAssignedActionFromPosition(lastRow + 1,
			    m_assignedOperationsModel, m_firstUnconfigurableToolbarElement);
		}
		/* Insert entries. */
		for (const ActionListModel::Entry &entry : entries) {
			QAction *action = qobject_cast<QAction *>(entry.controlObject);
			if (Q_UNLIKELY(action == Q_NULLPTR)) {
				Q_ASSERT(0);
				return;
			}
			insertToolBarAction(m_toolBar, before, action);
		}
	}

	if (lastRow == -1) {
		m_assignedOperationsModel.insertActions(entries);
	} else {
		m_assignedOperationsModel.insertActions(lastRow + 1, entries);
	}
}

void DlgToolBar::removeSelectedAssignedOperations(void)
{
	QModelIndexList slctIdxs =
	    m_ui->assignedOperationsView->selectionModel()->selectedRows(
	        ActionListModel::COL_ACTION_NAME);
	if (Q_UNLIKELY(slctIdxs.isEmpty())) {
		/* Nothing to do. */
		return;
	}

	/* Clear the selection before removing content. */
	m_ui->assignedOperationsView->clearSelection();

	QList<int> removedRows;
	for (const QModelIndex &index : slctIdxs) {
		if (index.isValid()) {
			removedRows.append(index.row());
		}
	}

	if (m_toolBar != Q_NULLPTR) {
		for (int row : removedRows) {
			QAction *action = qobject_cast<QAction *>(m_assignedOperationsModel.entryAt(row).controlObject);
			if (action != Q_NULLPTR) {
				m_toolBar->removeAction(action);
			}
		}
	}

	m_assignedOperationsModel.deleteActions(removedRows);
}

void DlgToolBar::radioButtonsToggled(bool checked)
{
	QRadioButton *button = qobject_cast<QRadioButton *>(sender());
	if (Q_UNLIKELY(button == Q_NULLPTR)) {
		return;
	}

	if (!checked) {
		/* Ignore unchecked values. */
		return;
	}

	if (m_toolBar != Q_NULLPTR) {
		if (button == m_ui->iconOnlyRadio) {
			m_toolBar->setToolButtonStyle(qtToolButtonStyle(
			    PrefsSpecific::ICON_ONLY));
		} else if (button == m_ui->textBesideIconRadio) {
			m_toolBar->setToolButtonStyle(qtToolButtonStyle(
			    PrefsSpecific::TEXT_BESIDE_ICON));
		} else if (button == m_ui->textUnderIconRadio) {
			m_toolBar->setToolButtonStyle(qtToolButtonStyle(
			    PrefsSpecific::TEXT_UNDER_ICON));
		}
	}
}

void DlgToolBar::handleAssignedDataChange(const QModelIndex &topLeft,
    const QModelIndex &bottomRight, const QVector<int> &roles)
{
	if ((roles.size() != 1) || (roles.at(0) != Qt::CheckStateRole)) {
		/* Ignore all but check state changes. */
		return;
	}

	if (Q_UNLIKELY(m_toolBar == Q_NULLPTR)) {
		return;
	}

	const int topRow = topLeft.row();
	const int bottomRow = bottomRight.row();

	for (int row = topRow; row <= bottomRow; ++row) {
		const ActionListModel::Entry entry = m_assignedOperationsModel.entryAt(row);
		QAction *action = qobject_cast<QAction *>(entry.controlObject);
		if (Q_UNLIKELY(action == Q_NULLPTR)) {
			Q_ASSERT(0);
			return;
		}
		/* Ignore selection change for separators. */
		if (!action->isSeparator()) {
			if (!entry.checked) {
				/* Remove unchecked from tool bar. */
				m_toolBar->removeAction(action);
			} else {
				/* Insert checked to tool bar. */
				QAction *before = activeAssignedActionFromPosition(row + 1,
				    m_assignedOperationsModel, m_firstUnconfigurableToolbarElement);
				insertToolBarAction(m_toolBar, before, action);
			}
		}
	}
}

void DlgToolBar::assignedRowsMoved(const QModelIndex &parent, int start, int end,
    const QModelIndex &destination, int row)
{
	if (Q_UNLIKELY(parent.isValid() || destination.isValid())) {
		Q_ASSERT(0);
		return;
	}

	if (Q_UNLIKELY(m_toolBar == Q_NULLPTR)) {
		return;
	}

	const int cnt = end - start + 1;
	const int newStart = (start < row) ? (row - cnt) : row;

	QAction *before = activeAssignedActionFromPosition(newStart + cnt,
	    m_assignedOperationsModel, m_firstUnconfigurableToolbarElement);

	/* Remove and newly insert moved actions. */
	for (int i = newStart; i < (newStart + cnt); ++i) {
		const bool checked = m_assignedOperationsModel.entryAt(i).checked;
		QAction *action = qobject_cast<QAction *>(m_assignedOperationsModel.entryAt(i).controlObject);
		if (Q_UNLIKELY(action == Q_NULLPTR)) {
			Q_ASSERT(0);
			return;
		}

		if (!action->isSeparator()) {
			if (checked) {
				m_toolBar->removeAction(action);

				insertToolBarAction(m_toolBar, before, action);
			}
		} else {
			/* Delete separator, create new and update model. */
			m_toolBar->removeAction(action);

			action = m_toolBar->insertSeparator(before);

			m_assignedOperationsModel.setSeparatorPointer(i, action);
		}
	}
}

class MoveDescription {
public:
	MoveDescription(int s, int c, int d)
	    : src(s), cnt(c), dest(d)
	{ }

	int src;
	int cnt;
	int dest;
};

void DlgToolBar::moveSelectedAssignedOperationsUp(void)
{
	QList<int> rows;

	{
		QModelIndexList slctIdxs =
		    m_ui->assignedOperationsView->selectionModel()->selectedRows(
		        ActionListModel::COL_ACTION_NAME);
		if (Q_UNLIKELY(slctIdxs.isEmpty() ||
		    (slctIdxs.size() == m_assignedOperationsModel.rowCount()))) {
			/* Nothing to do. */
			return;
		}

		for (const QModelIndex &idx :  slctIdxs) {
			rows.append(idx.row());
		}
	}

	::std::sort(rows.begin(), rows.end());

	QList<MoveDescription> moves;
	{
		MoveDescription thisMove(-1, -1, -1);
		for (int row : rows) {
			if (thisMove.src < 0) {
				thisMove.src = row;
				thisMove.cnt = 1;
				thisMove.dest = (row >= 1) ? (row - 1) : 0;
			} else if ((thisMove.src + thisMove.cnt) == row) {
				++thisMove.cnt;
			} else {
				moves.append(thisMove);
#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0))
				const MoveDescription &lastMove = moves.constLast();
#else /* < Qt-5.6 */
				const MoveDescription &lastMove = moves.last();
#endif /* >= Qt-5.6 */
				thisMove = MoveDescription(row, 1, lastMove.dest + lastMove.cnt);
			}
		}
		moves.append(thisMove);
	}

	for (const MoveDescription &move : moves) {
		if (Q_UNLIKELY(move.src == move.dest)) {
			continue;
		}

		m_assignedOperationsModel.moveRows(QModelIndex(), move.src, move.cnt, QModelIndex(), move.dest);
	}

}

/*!
 * @brief Used for reverse sorting.
 *
 * @param[in] a Left value.
 * @param[in] b Right value.
 * @return True if a > b;
 */
static
bool greater(int a, int b)
{
	return a > b;
}

void DlgToolBar::moveSelectedAssignedOperationsDown(void)
{
	QList<int> rows;

	{
		QModelIndexList slctIdxs =
		    m_ui->assignedOperationsView->selectionModel()->selectedRows(
		        ActionListModel::COL_ACTION_NAME);
		if (Q_UNLIKELY(slctIdxs.isEmpty() ||
		    (slctIdxs.size() == m_assignedOperationsModel.rowCount()))) {
			/* Nothing to do. */
			return;
		}

		for (const QModelIndex &idx :  slctIdxs) {
			rows.append(idx.row());
		}
	}

	::std::sort(rows.begin(), rows.end(), greater);

	QList<MoveDescription> moves;
	{
		MoveDescription thisMove(-1, -1, -1);
		for (int row : rows) {
			if (thisMove.src < 0) {
				thisMove.src = row;
				thisMove.cnt = -1;
				thisMove.dest = ((row + 1) < m_assignedOperationsModel.rowCount()) ? (row + 1) : (m_assignedOperationsModel.rowCount() - 1);
			} else if ((thisMove.src + thisMove.cnt) == row) {
				--thisMove.cnt;
			} else {
				moves.append(thisMove);
#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0))
				const MoveDescription &lastMove = moves.constLast();
#else /* < Qt-5.6 */
				const MoveDescription &lastMove = moves.last();
#endif /* >= Qt-5.6 */
				thisMove = MoveDescription(row, -1, lastMove.dest + lastMove.cnt);
			}
		}
		moves.append(thisMove);
	}

	for (const MoveDescription &move : moves) {
		if (Q_UNLIKELY(move.src == move.dest)) {
			continue;
		}

		m_assignedOperationsModel.moveRows(QModelIndex(), move.src + move.cnt + 1, -move.cnt, QModelIndex(), move.dest + 1);
	}
}

void DlgToolBar::insertSeparator(void)
{
	/* Find last selected entry. */
	int lastRow = lastSelectedRow(m_ui->assignedOperationsView,
	    &m_assignedOperationsModel);

	ActionListModel::Entry entry;

	if (m_toolBar != Q_NULLPTR) {
		/* Find position in tool bar. */
		QAction *before = m_firstUnconfigurableToolbarElement;
		if (lastRow != -1) {
			before = activeAssignedActionFromPosition(lastRow + 1,
			    m_assignedOperationsModel, m_firstUnconfigurableToolbarElement);
		}
		/* Create the separator. */
		QAction *action = m_toolBar->insertSeparator(before);
		/* Construct entry. */
		entry.type = ActionListModel::Entry::TYPE_ACTION;
		entry.checked = true;
		entry.controlObject = action;
	}

	if (lastRow == -1) {
		m_assignedOperationsModel.insertAction(entry);
	} else {
		m_assignedOperationsModel.insertAction(lastRow + 1, entry);
	}
}

void DlgToolBar::resetDefaults(void)
{
	if (m_toolBar != Q_NULLPTR) {
		removeToolBarActions(m_toolBar, m_firstUnconfigurableToolbarElement);
	}

	m_assignedOperationsModel.removeAllRows();

	for (QAction *action : m_defaultToolBarActions) {
		if (action != Q_NULLPTR) {
			if (m_toolBar != Q_NULLPTR) {
				insertToolBarAction(m_toolBar, m_firstUnconfigurableToolbarElement, action);
			}
			m_assignedOperationsModel.insertAction(ActionListModel::Entry(ActionListModel::Entry::TYPE_ACTION, true, action));
		} else {
			insertSeparator();
		}
	}

	m_ui->textUnderIconRadio->setChecked(true);
}

void DlgToolBar::initDlg(void)
{
	setIcons();

	m_ui->filterLine->setPlaceholderText(tr("Type to search."));
	m_ui->filterLine->setToolTip(tr("Enter sought expression."));
	m_ui->filterLine->setClearButtonEnabled(true);

	m_ui->descriptionText->setLineWrapMode(QPlainTextEdit::NoWrap);
	m_ui->descriptionText->setReadOnly(true);
	{
#define DESCRIPTION_LINES 6
		QFontMetrics metrics(m_ui->descriptionText->font());
		int rowHeight = metrics.lineSpacing() ;
		m_ui->descriptionText->setFixedHeight(DESCRIPTION_LINES * rowHeight);
#undef DESCRIPTION_LINES
	}

	connect(m_ui->filterLine, SIGNAL(textChanged(QString)),
	    this, SLOT(filterFound(QString)));

	connect(m_ui->categoryComboBox, SIGNAL(currentIndexChanged(int)),
	    this, SLOT(loadAvailableOperations(int)));

	m_ui->categoryComboBox->setModel(&m_availableMenuModel);

	m_availableOperationsProxyModel.setSortRole(Qt::DisplayRole);
	m_availableOperationsProxyModel.setSourceModel(&m_availableOperationsModel);

	m_ui->availableOperationsView->setModel(&m_availableOperationsProxyModel);
	m_ui->availableOperationsView->setSelectionMode(QAbstractItemView::ExtendedSelection);
	m_ui->availableOperationsView->setSelectionBehavior(QAbstractItemView::SelectRows);

	connect(m_ui->availableOperationsView->selectionModel(),
	    SIGNAL(selectionChanged(QItemSelection, QItemSelection)),
	    this, SLOT(handleAvailableSelectionChange()));

	m_ui->assignedOperationsView->setModel(&m_assignedOperationsModel);
	m_ui->assignedOperationsView->setSelectionMode(QAbstractItemView::ExtendedSelection);
	m_ui->assignedOperationsView->setSelectionBehavior(QAbstractItemView::SelectRows);

	connect(m_ui->assignedOperationsView->selectionModel(),
	    SIGNAL(selectionChanged(QItemSelection, QItemSelection)),
	    this, SLOT(handleAssignedSelectionChange()));

	{
		TableKeyPressFilter *filter =
		    new (::std::nothrow) TableKeyPressFilter(m_ui->assignedOperationsView);
		if (filter != Q_NULLPTR) {
			filter->registerAction(Qt::Key_Delete,
			    &removeSelectedAssignedOperationsViaFilter, this, true);
			m_ui->assignedOperationsView->installEventFilter(filter);
		}
	}
	m_ui->assignedOperationsView->installEventFilter(
	    new (::std::nothrow) TableSpaceSelectionFilter(
	        ActionListModel::COL_ACTION_NAME, m_ui->assignedOperationsView));

	/* Enable drag and drop on assigned list. */
	m_ui->assignedOperationsView->setAcceptDrops(true);
	m_ui->assignedOperationsView->setDragEnabled(true);
	m_ui->assignedOperationsView->setDragDropOverwriteMode(false);
	m_ui->assignedOperationsView->setDropIndicatorShown(true);
	m_ui->assignedOperationsView->setDragDropMode(QAbstractItemView::InternalMove);
	m_ui->assignedOperationsView->setDefaultDropAction(Qt::MoveAction);

	m_assignedOperationsModel.setChecking(true);

	connect(m_ui->assignButton, SIGNAL(clicked()),
	    this, SLOT(assignSelectedAvailableOperations()));
	connect(m_ui->removeButton, SIGNAL(clicked()),
	    this, SLOT(removeSelectedAssignedOperations()));

	m_ui->assignButton->setEnabled(false);
	m_ui->removeButton->setEnabled(false);

	m_ui->iconOnlyRadio->setChecked(false);
	m_ui->textBesideIconRadio->setChecked(false);
	m_ui->textUnderIconRadio->setChecked(false);

	connect(m_ui->iconOnlyRadio, SIGNAL(toggled(bool)),
	    this, SLOT(radioButtonsToggled(bool)));
	connect(m_ui->textBesideIconRadio, SIGNAL(toggled(bool)),
	    this, SLOT(radioButtonsToggled(bool)));
	connect(m_ui->textUnderIconRadio, SIGNAL(toggled(bool)),
	    this, SLOT(radioButtonsToggled(bool)));

	connect(&m_assignedOperationsModel, SIGNAL(dataChanged(QModelIndex, QModelIndex, QVector<int>)),
	    this, SLOT(handleAssignedDataChange(QModelIndex, QModelIndex, QVector<int>)));
	connect(&m_assignedOperationsModel, SIGNAL(rowsMoved(QModelIndex, int, int, QModelIndex, int)),
	    this, SLOT(assignedRowsMoved(QModelIndex, int, int, QModelIndex, int)));

	connect(m_ui->insertSeparatorButton, SIGNAL(clicked()),
	    this, SLOT(insertSeparator()));
	connect(m_ui->defaultsButton, SIGNAL(clicked()),
	    this, SLOT(resetDefaults()));

	connect(m_ui->upButton, SIGNAL(clicked()),
	    this, SLOT(moveSelectedAssignedOperationsUp()));
	connect(m_ui->downButton, SIGNAL(clicked()),
	    this, SLOT(moveSelectedAssignedOperationsDown()));
}

void DlgToolBar::setIcons(void)
{
	m_ui->assignButton->setIcon(
	    IconContainer::construcIcon(IconContainer::ICON_RIGHT));

	m_ui->removeButton->setIcon(
	    IconContainer::construcIcon(IconContainer::ICON_LEFT));

	m_ui->upButton->setIcon(
	    IconContainer::construcIcon(IconContainer::ICON_UP));

	m_ui->downButton->setIcon(
	    IconContainer::construcIcon(IconContainer::ICON_DOWN));
}

void DlgToolBar::removeSelectedAssignedOperationsViaFilter(QObject *dlgPtr)
{
	if (Q_UNLIKELY(dlgPtr == Q_NULLPTR)) {
		return;
	}

	DlgToolBar *dlg = qobject_cast<DlgToolBar *>(dlgPtr);
	if (Q_UNLIKELY(dlg == Q_NULLPTR)) {
		Q_ASSERT(0);
		return;
	}

	dlg->removeSelectedAssignedOperations();
}

void insertToolBarActions(QToolBar *toolBar,
    const QList<QAction *> &actionList, QAction *before)
{
	if (Q_UNLIKELY(toolBar == Q_NULLPTR)) {
		Q_ASSERT(0);
		return;
	}

	for (QAction *action : actionList) {
		if (action != Q_NULLPTR) {
			insertToolBarAction(toolBar, before, action);
		} else {
			toolBar->insertSeparator(before);
		}
	}
}

/*!
 * @brief Find actions.
 *
 * @param[in] parent Parent action.
 * @param[in] entryList Stored action list.
 * @param[in] ok True on success.
 * @return List of actions.
 */
static
QList<QAction *> findActions(const QObject *parent,
    const Json::ActionEntryList &entryList, bool *ok = Q_NULLPTR)
{
	QList<QAction *> actions;

	if (Q_UNLIKELY(parent == Q_NULLPTR)) {
		return actions;
	}

	bool iOk = true;

	for (const Json::ActionEntry &entry : entryList) {
		if (entry.checked()) {
			switch (entry.type()) {
			case Json::ActionEntry::TYPE_ACTION:
				{
					const QString &objectName = entry.objectName();
					if (!objectName.isEmpty()) {
						QAction *action = parent->findChild<QAction *>(objectName);
						if (action != Q_NULLPTR) {
							actions.append(action);
						} else {
							iOk = false;
						}
					}
				}
				break;
			case Json::ActionEntry::TYPE_SEPARATOR:
				actions.append(Q_NULLPTR);
				break;
			default:
				iOk = false;
				break;
			}
		}
	}

	if (ok != Q_NULLPTR) {
		*ok = iOk;
	}
	return actions;
}

void loadToolBarActions(QObject *actionParent, QToolBar *toolBar,
    const QList<QAction *> &dfltActionList, QAction *before)
{
	if (Q_UNLIKELY(toolBar == Q_NULLPTR)) {
		Q_ASSERT(0);
		return;
	}

	bool iOk = false;
	QList<QAction *> actionList;
	Json::ActionEntryList storedList = loadStoredList(&iOk);
	if (Q_UNLIKELY(!iOk)) {
		goto fail;
	}

	iOk = false;
	actionList = findActions(actionParent, storedList, &iOk);
	if (Q_UNLIKELY(!iOk)) {
		goto fail;
	}

	insertToolBarActions(toolBar, actionList, before);
	return;

fail:
	/* Load defaults. */
	insertToolBarActions(toolBar, dfltActionList, before);
}

Qt::ToolButtonStyle toolBarButtonStyle(void)
{
	qint64 val = PrefsSpecific::TEXT_UNDER_ICON;
	ignoreRetVal(
	    GlobInstcs::prefsPtr->intVal(MAIN_TOP_TOOLBAR_STYLE_KEY, val));
	return qtToolButtonStyle(val);
}
