#ifndef CVVISUAL_STFLENGINE_HPP
#define CVVISUAL_STFLENGINE_HPP

#include <math.h>
#include <vector>
#include <QString>
#include <QMap>
#include <QHash>
#include <QSet>
#include <QRegExp>
#include <QSettings>

#include <map>
#include <set>
#include <iostream>
#include <functional>
#include <algorithm>
#include <utility>
#include <iterator>

#include "stringutils.hpp"
#include "element_group.hpp"
#include "../qtutil/util.hpp"


// WORKAROUND:
//   - Qt::SplitBehavior introduced in 5.14, QString::SplitBehavior was removed in Qt6
//   - Support required part of Qt::SplitBehavior from 5.15 for older Qt versions
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
namespace Qt {
	static constexpr QString::SplitBehavior SkipEmptyParts = QString::SkipEmptyParts;
}
#endif


namespace cvv
{
namespace stfl
{

/**
 * @brief Parses and interprets text queries on its inherited elements.
 * The queries are written in a simple filter language.
 * @see http://cvv.mostlynerdless.de/ref/filterquery-ref.html
 */
template <typename Element> class STFLEngine
{
      public:
	/**
	 * @brief Constructs a new Engine.
	 * @param id id for storing the last queries.
	 * @note Use the appropriate setters to add filters, etc.
	 */
	STFLEngine(QString id): id{id}
	{
	}

	/**
	 * @brief Constructs (and initializes) a new engine.
	 * 
	 * Use this constructor only if you want to have control about everything.
	 * Consider using the simple constructor in combination with the add*
	 * commands instead.
	 *
	 * @param id id for storing the last queries.
	 * @param filterFuncs map mapping a filter command to a filter function
	 * @param filterPoolFuncs map mapping a filter command to a filter pool
	 * function (that returns a value the filter command filters on for a given
	 * element)
	 * @param filterCSFuncs map mapping a filter cs command (it allows comma
	 * separated arguments
	 * to a filter function
	 * @param filterCSPoolFuncs map mapping a filter cs command to a filter
	 * pool function
	 * @param sortFuncs map mapping a sort command to sort function
	 * @param groupFuncs map mapping a group command to a grouping function
	 * (a function returning a grooup name for a given element)
	 */
	STFLEngine(
	    QMap<QString, std::function<bool(const QString &, const Element &)>>
	        filterFuncs,
	    QMap<QString, std::function<QString(const Element &)>>
	        filterPoolFuncs,
	    QMap<QString, std::function<bool(const QStringList &,
	                                     const Element &)>> filterCSFuncs,
	    QMap<QString, std::function<QSet<QString>(const Element &)>>
	        filterCSPoolFuncs,
	    QMap<QString, std::function<int(const Element &, const Element &)>>
	        sortFuncs,
	    QMap<QString, std::function<QString(const Element &)>> groupFuncs)
	    : id{id}, filterFuncs{ filterFuncs }, filterPoolFuncs{ filterPoolFuncs },
	      filterCSFuncs{ filterCSFuncs },
	      filterCSPoolFuncs{ filterCSPoolFuncs }, sortFuncs{ sortFuncs },
	      groupFuncs{ groupFuncs }
	{
		initSupportedCommandsList();
	}

	/**
	 * @brief Adds a new element and updates the string pools.
	 * @param element new element
	 */
	void addNewElement(Element element)
	{
		elements.append(element);
		updateFilterPools(element);
	}

	/**
	 * @brief Returns a number of suggestions for a given query.
	 * E.g. query = "#sourt" => "#sort"
	 * @param query given query
	 * @param number maximum number of suggestions
	 * @return suggestions for the given query
	 */
	QStringList getSuggestions(QString _query, size_t number = 50)
	{
		QString query(_query);
		bool addedRaw = false;
		if (!query.startsWith("#"))
		{
			query = "#raw " + query;
			addedRaw = true;
		}

		QStringList cmdStrings = query.split("#");
		QStringList _cmdStrings = _query.split("#");
		QString lastCmdString;
		if (cmdStrings.empty())
		{
			lastCmdString = "";
		}
		else
		{
			lastCmdString = cmdStrings[cmdStrings.size() - 1];
		}
		QStringList suggs =
		    getSuggestionsForCmdQuery(lastCmdString, number);
		for (auto &sugg : suggs)
			sugg = sugg.trimmed();
		for (auto &str : _cmdStrings)
			str = str.trimmed();
		for (int i = 0; i < suggs.size(); i++)
		{
			if (_cmdStrings.empty())
			{
				suggs[i] = suggs[i].right(suggs[i].size() - 1);
			}
			else
			{
				_cmdStrings[_cmdStrings.size() - 1] = suggs[i];
				suggs[i] = _cmdStrings.join(" #").trimmed();
			}
			if (addedRaw)
			{
				replaceIfStartsWith(suggs[i], "raw ", "");
			}
		}
		return suggs;
	}

	/**
	 * @brief Executes the given query on its elements.
	 * @param query given query
	 * @return the resulting element groups
	 */
	std::vector<ElementGroup<Element>> query(QString query)
	{
		lastQuery = query;
		if (!query.startsWith("#"))
		{
			query = "#raw " + query;
		}
		QList<Element> elemList;
		QStringList cmdStrings =
		    query.split("#", Qt::SkipEmptyParts);
		elemList = executeFilters(elements, cmdStrings);
		elemList = executeSortCmds(elemList, cmdStrings);
		auto groups = executeGroupCmds(elemList, cmdStrings);
		executeAdditionalCommands(groups, cmdStrings);
		addQueryToStore(query);
		return groups;
	}

	/**
	 * @brief Reexecutes the last query.
	 * @note If no query had been executed before, a blank string is used.
	 *
	 * @return query result.
	 */
	std::vector<ElementGroup<Element>> reexecuteLastQuery()
	{
		return query(lastQuery);
	}

	/**
	 * @brief Adds the new elements to the elements already inherited by
	 *this engine.
	 *
	 * @param newElements new elements
	 */
	void addElements(QList<Element> newElements)
	{
		for (Element &elem : newElements)
		{
			addNewElement(elem);
		}
	}

	/**
	 * @brief Adds the new elements to the elements already inherited by
	 * this engine.
	 *
	 * @param newElements new elements
	 */
	void addElements(std::vector<Element> newElements)
	{
		for (Element &elem : newElements)
		{
			addNewElement(elem);
		}
	}
									   
	/**
	 * @brief Sets the elements inherited by this engine.
	 *
	 * @param newElements new elements, now inherited by this engine
	 */
	void setElements(QList<Element> newElements)
	{
		elements.clear();
		for (Element &elem : newElements)
		{
			addNewElement(elem);
		}
		reinitFilterPools();
	}

	/**
	 * @brief Sets the elements inherited by this engine.
	 *
	 * @param newElements new elements, now inherited by this engine
	 */
	void setElements(std::pair<QList<Element>, QList<Element>> newElements)
	{
		elements.clear();
		for (Element &elem : newElements.first)
		{
			addNewElement(elem);
		}
		for (Element &elem : newElements.second)
		{
			addNewElement(elem);
		}
		reinitFilterPools();
	}

	/**
	 * @brief Sets the filter function for the given filter command.
	 * @param command given filter command
	 * @param func filter function
	 */
	void setFilterFunc(
	    QString command,
	    std::function<bool(const QString &, const Element &)> func)
	{
		filterFuncs[command] = func;
		initSupportedCommandsList();
		reinitFilterPools();
	}

	/**
	 * @brief Sets the filter pool function for the given filter command.
	 * @param command given filter command
	 * @param func filter pool function
	 */
	void setFilterPoolFunc(QString command,
	                       std::function<QString(const Element &)> func)
	{
		filterPoolFuncs[command] = func;
		initSupportedCommandsList();
		reinitFilterPools();
	}

	/**
	 * @brief Sets the filter cs function for the given filter command.
	 * A filter cs is a filter that accepts several comma separated
	 * arguments.
	 * @param command given filter command
	 * @param func filter function
	 */
	void setFilterCSFunc(
	    QString command,
	    std::function<bool(const QStringList &, const Element &)> func)
	{
		filterCSFuncs[command] = func;
		initSupportedCommandsList();
		reinitFilterPools();
	}

	/**
	 * @brief Sets the filter cs pool function for the given filter command.
	 * A filter cs is a filter that accepts several comma separated
	 * arguments.
	 * @param command given filter command
	 * @param func filter pool function
	 */
	void
	setFilterCSPoolFunc(QString command,
	                    std::function<QSet<QString>(const Element &)> func)
	{
		filterCSPoolFuncs[command] = func;
		initSupportedCommandsList();
		reinitFilterPools();
	}

	/**
	 * @brief Derives a basic filter, group and sort function from the given
	 * function.
	 * @note It's slightly slower than just creating the methods on your on
	 * and adding them
	 * with the appropriate setters.
	 * @param command filter, group and sort command name
	 * @param func function returning a string for an element, which is used
	 * for filtering,
	 * sorting and grouping
	 * @param withFilterCS does the filter allow several, comma separated
	 * arguments?
	 */
	void addStringCmdFunc(QString command,
	                      std::function<QString(const Element &)> func,
	                      bool withFilterCS = true)
	{
		if (withFilterCS)
		{
			filterCSFuncs[command] = [func](const QStringList &args,
			                                const Element &elem)
			{ return args.contains(func(elem)); };
			filterCSPoolFuncs[command] = [func](const Element &elem)
			{ return qtutil::createStringSet(func(elem)); };
		}
		else
		{
			filterFuncs[command] = [func](const QString &query,
			                              const Element &elem)
			{ return query == func(elem); };
			filterPoolFuncs[command] = [func](const Element &elem)
			{ return func(elem); };
		}
		sortFuncs[command] = [func](const Element &elem1,
		                            const Element &elem2)
		{ return func(elem1) < func(elem2); };
		groupFuncs[command] = [func](const Element &elem)
		{ return func(elem); };
		initSupportedCommandsList();
		reinitFilterPools();
	}

	/**
	 * @brief Derives a basic (int based) filter, group and sort function
	 * from the given function.
	 * @note It's slightly slower than just creating the methods on your on
	 * and adding them
	 * with the appropriate setters.
	 * @param command filter, group and sort command name
	 * @param func function returning an integer for an element, which is
	 * used for filtering,
	 * sorting and grouping
	 * @param withFilterCS does the filter allow several, comma separated
	 * arguments?
	 * @param widthRangeCmd add `[command]_range` (inclusive) range filter?
	 * @note Range filter only works if withFilterCS is true.
	 */
	void addIntegerCmdFunc(QString command,
	                       std::function<int(const Element &)> func,
	                       bool withFilterCS = true,
	                       bool withRangeCmd = true)
	{
		if (withFilterCS)
		{
			filterCSFuncs[command] = [func](const QStringList &args,
			                                const Element &elem)
			{ return args.contains(QString::number(func(elem))); };
			filterCSPoolFuncs[command] = [func](const Element &elem)
			{
				return qtutil::createStringSet(
				    QString::number(func(elem)));
			};

			if (withRangeCmd)
			{
				QString range_cmd = command + "_range";
				auto filterFunc = filterCSFuncs[command];
				filterCSFuncs[range_cmd] = [func, filterFunc](
				    const QStringList &args,
				    const Element &elem)
				{
					if (args.size() < 2)
					{
						return true;
					}
					bool ok = true;
					long first = args[0].toLong(&ok);
					long second = args[1].toLong(&ok);
					int intElem = func(elem);
					return !ok || (first <= intElem &&
					               intElem <= second);
				};
				filterCSPoolFuncs[range_cmd] =
				    filterCSPoolFuncs[command];
			}
		}
		else
		{
			filterFuncs[command] = [func](const QString &query,
			                              const Element &elem)
			{ return query.toInt() == func(elem); };
			filterPoolFuncs[command] = [func](const Element &elem)
			{ return QString::number(func(elem)); };
		}
		sortFuncs[command] = [func](const Element &elem1,
		                            const Element &elem2)
		{ return func(elem1) < func(elem2); };
		groupFuncs[command] = [func, command](const Element &elem)
		{ return QString(command + " %1").arg(func(elem)); };
		initSupportedCommandsList();
		reinitFilterPools();
	}

	/**
	 * @brief Derives a basic (float based) filter, group and sort function
	 * from the given function.
	 * @note It's slightly slower than just creating the methods on your on
	 * and adding them
	 * with the appropriate setters.
	 * @param command filter, group and sort command name
	 * @param func function returning a float for an element, which is used
	 * for filtering,
	 * sorting and grouping
	 * @param withFilterCS does the filter allow several, comma separated
	 * arguments?
	 * @param widthRangeCmd add `[command]_range` (inclusive) range filter?
	 * @note Range filter only works if withFilterCS is true.
	 */
	void addFloatCmdFunc(QString command,
	                     std::function<float(const Element &)> func,
	                     bool withFilterCS = true, bool withRangeCmd = true)
	{
		if (withFilterCS)
		{
			filterCSFuncs[command] = [func](const QStringList &args,
			                                const Element &elem)
			{ return args.contains(QString::number(func(elem))); };
			filterCSPoolFuncs[command] = [func](const Element &elem)
			{
				return qtutil::createStringSet(
				    QString::number(func(elem)));
			};

			if (withRangeCmd)
			{
				QString range_cmd = command + "_range";
				auto filterFunc = filterCSFuncs[command];
				filterCSFuncs[range_cmd] = [func, filterFunc](
				    const QStringList &args,
				    const Element &elem)
				{
					if (args.size() < 2)
					{
						return true;
					}
					bool ok = true;
					long first = args[0].toDouble(&ok);
					long second = args[1].toDouble(&ok);
					float floatElem = func(elem);
					return !ok || (first <= floatElem &&
					               floatElem <= second);
				};
				filterCSPoolFuncs[range_cmd] =
				    filterCSPoolFuncs[command];
			}
		}
		else
		{
			filterFuncs[command] = [func](const QString &query,
			                              const Element &elem)
			{ return query.toFloat() == func(elem); };
			filterPoolFuncs[command] = [func](const Element &elem)
			{ return QString::number(func(elem)); };
		}
		sortFuncs[command] = [func](const Element &elem1,
		                            const Element &elem2)
		{ return func(elem1) < func(elem2); };
		groupFuncs[command] = [func, command](const Element &elem)
		{ return QString(command + " %1").arg(func(elem)); };
		initSupportedCommandsList();
		reinitFilterPools();
	}

	/**
	 * @brief Add an additional command.
	 * Add a command which function gets only executed once per query execution.
	 * @param name command name
	 * @param func the associated function, that takes the parameters and the
	 * element groups resulting from the filter, group and sort commands.
	 */
	void addAdditionalCommand(QString name, 
			std::function<void(QStringList, std::vector<ElementGroup<Element>>&)> func,
			QStringList avParameters = QStringList{})
	{
		additionalCommandFuncs[name] = func;
		additionalCommandPools[name] = avParameters;
	}

	/**
	 * @brief Removes the elements that match the given function.
	 * @param matchFunc given match function
	 */
	void removeElements(std::function<bool(const Element &)> matchFunc)
	{
		auto newEnd =
		    std::remove_if(elements.begin(), elements.end(), matchFunc);
		elements.erase(newEnd, elements.end());
		reinitFilterPools();
	}

private:
	QString id;
	QSettings settings{"CVVisual", QSettings::IniFormat};
	QList<Element> elements;
	QString lastQuery = "";
	QStringList supportedCmds;
	QMap<QString, std::function<bool(const QString &, const Element &)>>
	filterFuncs;
	QMap<QString, std::function<QString(const Element &)>> filterPoolFuncs;
	QHash<QString, QSet<QString>> filterPool;

	QMap<QString, std::function<bool(const QStringList &, const Element &)>>
	filterCSFuncs;
	QMap<QString, std::function<QSet<QString>(const Element &)>>
	filterCSPoolFuncs;
	QHash<QString, QSet<QString>> filterCSPool;

	QMap<QString, std::function<int(const Element &, const Element &)>>
	sortFuncs;
	QMap<QString, std::function<QString(const Element &)>> groupFuncs;

	QMap<QString, std::function<void(QStringList,
			std::vector<ElementGroup<Element>>&)>> additionalCommandFuncs;
	QMap<QString, QStringList> additionalCommandPools;

	const int MAX_NUMBER_OF_STORED_CMDS = 200;

	QList<Element> executeFilters(const QList<Element> &elements,
	                              const QStringList &cmdStrings)
	{
		std::vector<std::function<bool(const Element &)>> filters;

		for (const QString &cmdString : cmdStrings)
		{
			using namespace std::placeholders;
			QStringList arr =
			    cmdString.split(" ", Qt::SkipEmptyParts);
			QString cmd;
			if (arr.empty())
			{
				cmd = "";
			}
			else
			{
				cmd = arr.takeFirst();
			}
			if (arr.empty())
				continue;
			if (isFilterCmd(cmd))
			{
				QString argument = arr.join(" ");
				filters.emplace_back(
				    std::bind(filterFuncs[cmd], argument, _1));
			}
			else if (isFilterCSCmd(cmd))
			{
				QStringList arguments = arr.join("").split(
				    ",", Qt::SkipEmptyParts);
				std::for_each(arguments.begin(),
				              arguments.end(), [](QString &str)
				{ str.replace("\\,", ","); });
				filters.emplace_back(std::bind(
				    filterCSFuncs[cmd], arguments, _1));
			}
		}
		if (filters.empty())
		{
			return elements;
		}
		QList<Element> retList;
		// copy if all filters match
		using StringFilter = std::function<bool(const Element &)>;
		auto copy_if = [&](const Element &element)
		{
			// find in filters
			auto find_if = [&](StringFilter filter)
			{
				return !filter(element);
			};
			auto returnval =
			    std::find_if(filters.begin(), filters.end(),
			                 find_if) == filters.end();
			return returnval;
		};
		std::copy_if(elements.begin(), elements.end(),
		             std::back_inserter(retList), copy_if);
		return retList;
	}

	QList<Element> executeSortCmds(const QList<Element> &elements,
	                               const QStringList &cmdStrings)
	{
		QList<std::pair<QString, bool>> sortCmds;
		for (QString cmdString : cmdStrings)
		{
			QStringList arr =
			    cmdString.split(" ", Qt::SkipEmptyParts);
			if (arr.size() < 2)
			{
				continue;
			}
			QString cmd = arr.takeFirst();
			if (arr[0] == "by")
			{
				arr.removeFirst();
			}
			arr = arr.join(" ").split(",", Qt::SkipEmptyParts);
			for (QString cmdPart : arr)
			{
				cmdPart = cmdPart.trimmed();
				QStringList cmdPartList = cmdPart.split(" ");
				if (cmdPartList.empty())
				{
					continue;
				}
				cmdPart = cmdPartList[0];
				bool asc = true;
				if (cmdPartList.size() >= 2)
				{
					asc = cmdPartList[1] == "asc";
				}
				if (isSortCmd(cmdPart))
				{
					sortCmds.append(
					    std::make_pair(cmdPart, asc));
				}
			}
		}
		QList<Element> resList(elements);
		for (auto sortCmd : sortCmds)
		{
			if (sortCmd.second)
			{
				auto sortFunc = sortFuncs[sortCmd.first];
				std::stable_sort(resList.begin(), resList.end(),
				            [&](const Element &elem1,
				                const Element &elem2)
				{ return sortFunc(elem1, elem2); });
			}
			else
			{
				auto sortFunc = sortFuncs[sortCmd.first];
				std::stable_sort(resList.begin(), resList.end(),
				            [&](const Element &elem1,
				                const Element &elem2)
				{ return sortFunc(elem2, elem1); });
			}
		}
		return resList;
	}

	/**
	 * @note I use std::vector here, as QList does strange things...
	 */
	std::vector<ElementGroup<Element>>
	executeGroupCmds(const QList<Element> &elements,
	                 const QStringList &cmdStrings)
	{
		QStringList groupCmds;
		for (QString cmdString : cmdStrings)
		{
			QStringList arr =
			    cmdString.split(" ", Qt::SkipEmptyParts);
			if (arr.size() < 2)
			{
				continue;
			}
			QString cmd = arr.takeFirst();
			if (cmd != "group")
			{
				continue;
			}
			if (arr[0] == "by")
			{
				arr.removeFirst();
			}
			arr = arr.join("").split(",", Qt::SkipEmptyParts);
			for (QString cmdPart : arr)
			{
				QStringList cmdPartList = cmdPart.split(" ");
				if (cmdPartList.empty() ||
				    !isGroupCmd(cmdPartList[0]))
				{
					continue;
				}
				groupCmds.append(cmdPartList[0]);
			}
		}
		std::vector<ElementGroup<Element>> groupList;
		std::map<QString, QList<Element>> groups{};
		for (auto &element : elements)
		{
			QString name = "";
			for (auto &groupCmd : groupCmds)
			{
				name.append("\\|" +
				            groupFuncs[groupCmd](element));
			}
			if (groups.count(name) == 0)
			{
				groups[name] = QList<Element>();
			}
			groups[name].push_back(element);
		}
		for (auto it = groups.begin(); it != groups.end(); ++it)
		{
			ElementGroup<Element> elementGroup(
			    it->first.split("\\|", Qt::SkipEmptyParts),
			    it->second);
			groupList.push_back(elementGroup);
		}
		return groupList;
	}
	
	void executeAdditionalCommands(std::vector<ElementGroup<Element>> &groups, QStringList cmdStrings)
	{
		cmdStrings.removeDuplicates();
		for (QString cmdString : cmdStrings)
		{
			QStringList arr =
			    cmdString.split(" ", Qt::SkipEmptyParts);
			if (arr.isEmpty())
			{
				continue;
			}
			QString cmd = arr.takeFirst();
			if (!isAdditionalCmd(cmd))
			{
				continue;
			}
			arr = arr.join("").split(",", Qt::SkipEmptyParts);
			additionalCommandFuncs[cmd](arr, groups);
		}
	}

	QStringList getSuggestionsForCmdQuery(const QString &cmdQuery,
	                                      size_t number)
	{
		QStringList tokens = cmdQuery.split(" ");
		QStringList suggs;
		if (tokens.empty())
		{
			return suggs;
		}
		bool hasByString = tokens.size() >= 2 && tokens[1] == "by";
		QString cmd = tokens[0];
		if (cmd == "group" || cmd == "sort")
		{
			int frontCut =
			    std::min(size_t(hasByString ? 2 : 1), size_t(tokens.size()));
			tokens = cmdQuery.split(" ", Qt::SkipEmptyParts)
			             .mid(frontCut, tokens.size());
			QStringList args = tokens.join(" ").split(
			    ",", Qt::SkipEmptyParts);
			args.removeDuplicates();
			for (auto &arg : args)
			{
				int wsLen = 0;
				for (wsLen = 0;
				     wsLen < arg.size() && arg[wsLen] == ' ';
				     wsLen++)
					;
				arg = arg.right(arg.size() - wsLen);
			}
			if (args.empty())
			{
				args.push_back(" ");
			}
			if (cmd == "sort")
			{
				suggs = getSuggestionsForSortCmd(args);
			}
			else
			{
				suggs = getSuggestionsForGroupCmd(args);
			}
		}
		else if (isFilterCmd(cmd) || isFilterCSCmd(cmd) || isAdditionalCmd(cmd))
		{
			tokens = tokens.mid(1, tokens.size());
			QString rejoined = tokens.join(" ");
			if (tokens.empty())
			{
				rejoined = " ";
			}
			if (isFilterCmd(cmd))
			{
				suggs =
				    getSuggestionsForFilterCmd(cmd, rejoined);
			}
			else
			{
				QStringList args = rejoined.split(
				    ",", Qt::SkipEmptyParts);
				if (isFilterCmd(cmd))
				{
					suggs = getSuggestionsForFilterCSCmd(cmd, args);
				}
				else
				{
					suggs = getSuggestionsForAdditionalCmd(cmd, args);
				}
			}
		}
		else
		{
			suggs = getSuggestionsForCmd(cmd);
		}
		if (isFilterCmd(cmd) || isFilterCSCmd(cmd) || isSortCmd(cmd) ||
		    isGroupCmd(cmd) || isAdditionalCmd(cmd))
		{
			for (auto &sugg : suggs)
			{
				sugg = cmd + " " + sugg;
			}
		}
		for (QString cmd : getStoredCmdsForInput(cmdQuery))
		{
			suggs.prepend(cmd);
		}
		return suggs.mid(0, number);
	}

	QStringList getSuggestionsForSortCmd(QStringList args)
	{
		QString last;
		if (args.empty())
		{
			last = "";
		}
		else
		{
			last = args[args.size() - 1];
		}
		QStringList pool(sortFuncs.keys());
		QStringList list;
		QStringList arr = last.split(" ");
		if (pool.contains(arr[0]))
		{
			list.append("asc");
			list.append("desc");
			if (arr.size() > 1)
			{
				list =
				    sortStringsByStringEquality(list, arr[1]);
			}
			else
			{
				list.prepend("");
			}
			for (auto &str : list)
			{
				str = arr[0] + " " + str;
			}
		}
		else
		{
			list = sortStringsByStringEquality(pool, last);
		}
		for (QString &item : list)
		{
			joinCommand(item, "sort by ", args);
		}
		return list;
	}

	QStringList getSuggestionsForGroupCmd(QStringList args)
	{
		QString last;
		if (args.empty())
		{
			last = "";
		}
		else
		{
			last = args[args.size() - 1];
		}
		QStringList pool(groupFuncs.keys());
		QStringList list = sortStringsByStringEquality(pool, last);
		for (QString &item : list)
		{
			joinCommand(item, "group by ", args);
		}
		return list;
	}

	QStringList getSuggestionsForFilterCmd(const QString &cmd,
	                                       const QString &argument)
	{
		QStringList pool(filterPool[cmd].values());
		return sortStringsByStringEquality(pool, argument);
	}

	QStringList getSuggestionsForFilterCSCmd(const QString &cmd,
	                                         QStringList args)
	{
		QString last;
		if (args.empty())
		{
			last = "";
		}
		else
		{
			last = args[args.size() - 1];
		}
		QStringList pool(filterCSPool[cmd].values());
		QStringList list = sortStringsByStringEquality(pool, last);
		for (QString &item : list)
		{
			joinCommand(item, cmd, args, true);
		}
		return list;
	}

	QStringList getSuggestionsForAdditionalCmd(const QString &cmd, const QStringList &args)
	{
		auto pool = additionalCommandPools[cmd];
		if (pool.isEmpty())
		{
			return QStringList(args.join(", "));
		}
		QString last;
		if (args.empty())
		{
			last = "";
		}
		else
		{
			last = args[args.size() - 1];
		}
		QStringList list = sortStringsByStringEquality(pool, last);
		for (QString &item : list)
		{
			joinCommand(item, cmd, args, true);
		}
		return list;
	}

	QStringList getSuggestionsForCmd(const QString &cmd)
	{
		return sortStringsByStringEquality(supportedCmds, cmd);
	}

	/**
	 * @brief Init the inherited list of supported commands.
	 * E.g. "#sort by", "#group by", "#[filter name]"
	 */
	void initSupportedCommandsList()
	{
		QStringList list;
		list.append(filterFuncs.keys());
		list.append(filterCSFuncs.keys());
		list.append(additionalCommandFuncs.keys());
		for (const auto &key : groupFuncs.keys())
		{
			list.append("group by " + key);
		}
		for (const auto &key : sortFuncs.keys())
		{
			list.append("sort by " + key);
		}
		supportedCmds = list;
	}

	void updateFilterPools(Element element)
	{
		auto it = filterPoolFuncs.begin();
		while (it != filterPoolFuncs.end())
		{
			filterPool[it.key()].insert(it.value()(element));
			++it;
		}
		auto it2 = filterCSPoolFuncs.begin();
		while (it2 != filterCSPoolFuncs.end())
		{
			filterCSPool[it2.key()].unite(it2.value()(element));
			++it2;
		}
	}

	void reinitFilterPools()
	{
		auto it = filterPoolFuncs.begin();
		while (it != filterPoolFuncs.end())
		{
			filterPool[it.key()].clear();
			for (auto element : elements)
			{
				filterPool[it.key()]
				    .insert(it.value()(element));
			}
			++it;
		}
		auto it2 = filterCSPoolFuncs.begin();
		while (it2 != filterCSPoolFuncs.end())
		{
			filterCSPool[it2.key()].clear();
			for (auto element : elements)
			{
				filterCSPool[it2.key()]
				    .unite(it2.value()(element));
			}
			++it2;
		}
	}

	void addQueryToStore(QString query)
	{
		QStringList storedCmds = getStoredCmds();
		QStringList cmds = query.split("#", Qt::SkipEmptyParts);
		cmds.removeDuplicates();
		for (QString cmd : cmds)
		{
			cmd = cmd.trimmed();
			if (cmd == "")
			{
				continue;
			}
			int index = storedCmds.indexOf(cmd);
			while (index != -1)
			{
				storedCmds.removeAt(index);
				index = storedCmds.indexOf(cmd);
			}
			if (storedCmds.size() >= MAX_NUMBER_OF_STORED_CMDS)
			{
				storedCmds.removeFirst();
			}
			storedCmds.append(cmd);
		}
		settings.setValue(QString("STFLEngine/%1/settings").arg(id), storedCmds);
	}

	QStringList getStoredCmds()
	{
		QString key = QString("STFLEngine/%1/settings").arg(id);
		if (!settings.contains(key))
		{
			return QStringList();
		}
		return settings.value(key).template value<QStringList>();
	}

	QStringList getStoredCmdsForInput(QString input)
	{
		QStringList store = getStoredCmds();
		QStringList retList;
		for (QString cmd : store)
		{
			if (cmd.startsWith(input) && cmd != input)
			{
				retList.prepend(cmd);
			}
		}
		return retList;
	}

	/**
	 * @brief It sorts the strings by their edit distance to the compareWith
	 * string in ascending order.
	 * @param strings strings to sort
	 * @param compareWith compare them with this string
	 * @return the sorted list
	 */
	QStringList sortStringsByStringEquality(const QStringList &strings,
	                                        QString compareWith)
	{
		QMap<int, QStringList> weightedStrings;
		auto compareWithWords =
		    compareWith.split(" ", Qt::SkipEmptyParts);
		for (const QString &str : strings)
		{
			int strEqu = 0xFFFFFF; // infinity...
			for (auto word :
			     str.split(" ", Qt::SkipEmptyParts))
			{
				auto wordA = word.leftJustified(15, ' ');
				for (const auto &word2 : compareWithWords)
				{
					auto wordB =
					    word2.leftJustified(15, ' ');
					int editDist =
					    editDistance(wordA, wordB);
					if (word.startsWith(word2) ||
					    word2.startsWith(word) ||
					    (word.size() > 2 &&
					     (word2.endsWith(word) ||
					      word.endsWith(word2))))
					{
						editDist /= 8;
					}
					strEqu = std::min(strEqu, editDist);
				}
			}
			if (!weightedStrings.contains(strEqu))
			{
				weightedStrings[strEqu] = QStringList();
			}
			weightedStrings[strEqu].push_back(str);
		}
		QStringList retList;
		for (auto &list : weightedStrings.values())
		{
			list.sort();
			retList.append(list);
		}
		return retList;
	}

	bool isSortCmd(const QString &cmd)
	{
		return sortFuncs.count(cmd) > 0;
	}

	bool isGroupCmd(const QString &cmd)
	{
		return groupFuncs.count(cmd) > 0;
	}

	bool isFilterCmd(const QString &cmd)
	{
		return filterFuncs.count(cmd) > 0;
	}

	bool isFilterCSCmd(const QString &cmd)
	{
		return filterCSFuncs.count(cmd) > 0;
	}

	bool isAdditionalCmd(const QString &cmd)
	{
		return additionalCommandFuncs.count(cmd) > 0;
	}
	
	void joinCommand(QString &item, const QString &cmd, QStringList args,
	                 bool omitCmd = false)
	{
		if (args.size() == 0)
		{
			item = "";
		}
		else
		{
			for (auto &arg : args)
				arg = arg.trimmed();
			args[args.size() - 1] = item;
			item = args.join(", ");
		}
		if (!omitCmd)
		{
			item = cmd + item;
		}
	}
};
}
}

#endif
