/***************************************************************************
                          rkcodecompletion  -  description
                             -------------------
    begin                : Thu Feb 21 2019
    copyright            : (C) 2004-2020 by Thomas Friedrichsmeier
    email                : thomas.friedrichsmeier@kdemail.net
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   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 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 ***************************************************************************/
#include "rkcodecompletion.h"

#include <ktexteditor/document.h>
#include <ktexteditor/editor.h>
#include <KLocalizedString>
#include <KActionCollection>

#include <QApplication>
#include <QKeyEvent>
#include <QIcon>
#include <QDir>
#include <QAction>

#include "../misc/rkcommonfunctions.h"
#include "../misc/rkstandardicons.h"
#include "../core/rfunctionobject.h"
#include "../core/robjectlist.h"
#include "../settings/rksettingsmodulecommandeditor.h"

#include "../debug.h"


class RKCompletionNotifierModel : public RKCompletionModelBase {
public:
	RKCompletionNotifierModel (RKCompletionManager *manager) : RKCompletionModelBase (manager) {};
	KTextEditor::Range completionRange (KTextEditor::View *, const KTextEditor::Cursor &position) override {
		return KTextEditor::Range (position, position);
	}
	/** reimplemented in order to receive notification, when completion is invoked by user shortcut. */
	void completionInvoked (KTextEditor::View*, const KTextEditor::Range&, KTextEditor::CodeCompletionModel::InvocationType invocationType) override {
		RK_TRACE (COMMANDEDITOR);

		if (invocationType == KTextEditor::CodeCompletionModel::UserInvocation) {
			// NOTE: Without this short timeout, the completion window will sometimes disappear, again, right away.
			QTimer::singleShot (50, manager, SLOT(userTriggeredCompletion()));
		}
	}
	QVariant data (const QModelIndex&, int) const override {
		return QVariant ();
	}
};

//////////////////////// RKCompletionManager //////////////////////

RKCompletionManager::RKCompletionManager(KTextEditor::View* view, const RKCodeCompletionSettings *settings) : QObject(view), settings(settings) {
	RK_TRACE (COMMANDEDITOR);

	_view = view;
	keep_active = false;
	user_triggered = false;
	ignore_next_trigger = false;
	update_call = true;
	cached_position = KTextEditor::Cursor (-1, -1);

	cc_iface = qobject_cast<KTextEditor::CodeCompletionInterface*> (view);
	if (cc_iface) {
		cc_iface->setAutomaticInvocationEnabled (false);
		completion_model = new RKCodeCompletionModel (this);
		file_completion_model = new RKFileCompletionModel (this);
		callhint_model = new RKCallHintModel (this);
		arghint_model = new RKArgumentHintModel (this);
		cc_iface->registerCompletionModel (new RKCompletionNotifierModel (this));  // (at least) one model needs to be registerd, so we will know, when completion was triggered by the user (shortcut)
		completion_timer = new QTimer (this);
		completion_timer->setSingleShot (true);
		connect (completion_timer, &QTimer::timeout, this, &RKCompletionManager::tryCompletion);
		connect (view->document (), &KTextEditor::Document::textInserted, this, &RKCompletionManager::textInserted);
		connect (view->document (), &KTextEditor::Document::textRemoved, this, &RKCompletionManager::textRemoved);
		connect (view->document (), &KTextEditor::Document::lineWrapped, this, &RKCompletionManager::lineWrapped);
		connect (view->document (), &KTextEditor::Document::lineUnwrapped, this, &RKCompletionManager::lineUnwrapped);
		connect (view, &KTextEditor::View::cursorPositionChanged, this, &RKCompletionManager::cursorPositionChanged);
		const QObjectList children = _view->children ();
		for (QObjectList::const_iterator it = children.constBegin(); it != children.constEnd (); ++it) {
			(*it)->installEventFilter (this);  // to handle Tab-key; installing on the view, alone, is not enough.
		}

		// HACK: I just can't see to make the object name completion model play nice with automatic invocation.
		//       However, there is no official way to invoke all registered models, manually. So we try to hack our way
		//       to a pointer to the default kate keyword completion model
		kate_keyword_completion_model = KTextEditor::Editor::instance ()->findChild<KTextEditor::CodeCompletionModel *> ();
		if (!kate_keyword_completion_model) kate_keyword_completion_model = view->findChild<KTextEditor::CodeCompletionModel *> (QString());

	} else {
		RK_ASSERT (false);  // Not a katepart?
	}
}

RKCompletionManager::~RKCompletionManager () {
	RK_TRACE (COMMANDEDITOR);
}

void RKCompletionManager::tryCompletionProxy () {
	if (cc_iface->isCompletionActive () || keep_active) {
		// Handle this in the next event cycle, as more than one event may trigger
		completion_timer->start (0);
	} else if (settings->autoEnabled ()) {
		completion_timer->start (settings->autoTimeout ());
	}
}

QString RKCompletionManager::currentCompletionWord () const {
	RK_TRACE (COMMANDEDITOR);

	if (symbol_range.isValid ()) return _view->document ()->text (symbol_range);
	return QString ();
}

void RKCompletionManager::userTriggeredCompletion () {
	RK_TRACE (COMMANDEDITOR);

	user_triggered = true;
	completion_timer->stop ();
	tryCompletion ();
	user_triggered = false;
}

void RKCompletionManager::tryCompletion () {
	RK_TRACE (COMMANDEDITOR);
	if (!cc_iface) {
		// NOTE: This should not be possible, because the connections  have not been set up in the constructor, in this case.
		RK_ASSERT (cc_iface);
		return;
	}
	if (ignore_next_trigger) {
		ignore_next_trigger = false;
		return;
	}

	KTextEditor::Document *doc = _view->document ();
	KTextEditor::Cursor c = _view->cursorPosition();
	cached_position = c;
	uint para=c.line(); int cursor_pos=c.column();

	QString current_line = doc->line (para);
	int start;
	int end;
	RKCommonFunctions::getCurrentSymbolOffset (current_line, cursor_pos-1, false, &start, &end);
	symbol_range = KTextEditor::Range (para, start, para, end);
	if (!user_triggered) {
		if (end > cursor_pos) {
			symbol_range = KTextEditor::Range ();   // Only hint when at the end of a word/symbol: https://mail.kde.org/pipermail/rkward-devel/2015-April/004122.html
		} else {
			if (doc->defaultStyleAt (c) == KTextEditor::dsComment) symbol_range = KTextEditor::Range ();	// do not hint while in comments
		}
	}

	QString word = currentCompletionWord ();
	if (user_triggered || (word.length () >= settings->autoMinChars ())) {
		QString filename;
		// as a very simple heuristic: If the current symbol starts with a quote, we should probably attempt file name completion, instead of symbol name completion
		if (word.startsWith ('\"') || word.startsWith ('\'') || word.startsWith ('`')) {
			symbol_range.setStart (KTextEditor::Cursor (symbol_range.start ().line (), symbol_range.start ().column () + 1));   // confine range to inside the quotes.
			filename = word.mid (1);
			word.clear ();
		}

		completion_model->updateCompletionList (word);
		file_completion_model->updateCompletionList (filename);
	} else {
		completion_model->updateCompletionList (QString ());
		file_completion_model->updateCompletionList (QString ());
	}
	RK_DEBUG(EDITOR, DL_DEBUG, "completion symbol range %d, %d -> %d, %d", symbol_range.start().line(), symbol_range.start().column(), symbol_range.end().line(), symbol_range.end().column());

	updateCallHint ();

	// update ArgHint.
	argname_range = KTextEditor::Range (-1, -1, -1, -1);
	// Named arguments are just like regular symbols, *but* we must require that they are preceeded by either a ',', or the opening '(', immediately.
	// Otherwise, they are an argument value expression, for sure.
	// We also assume (foolishly), that if we are on a new line, we're probably starting a new arg. TODO: Actually check this!
	if (callhint_model->currentFunction ()) {
		argname_range = symbol_range;
		for (int i = symbol_range.start ().column () - 1; i >= 0; --i) {
			QChar c = current_line.at (i);
			if (c == ',' || c == '(') {
				break;
			} else if (!c.isSpace ()) {
				argname_range = KTextEditor::Range (-1, -1, -1, -1);
			}
		}
	}
	arghint_model->updateCompletionList (callhint_model->currentFunction (), argname_range.isValid () ? doc->text (argname_range) : QString ());

	updateVisibility ();
}

bool isCode (KTextEditor::Document* doc, int line, int column) {
	KTextEditor::DefaultStyle style = doc->defaultStyleAt (KTextEditor::Cursor (line, column));
	if (style == KTextEditor::dsComment) return false;
	if (style == KTextEditor::dsString) return false;
	if (style == KTextEditor::dsChar) return false;
	return true;
}

void RKCompletionManager::updateCallHint () {
	RK_TRACE (COMMANDEDITOR);

	if (cc_iface->isCompletionActive () && !update_call) return;
	update_call = false;

	int line = cached_position.line () + 1;
	QString full_context;
	int potential_symbol_end = -2;
	int parenthesis_level = 0;
	int prefix_offset = 0;
	KTextEditor::Document *doc = _view->document ();
	while (potential_symbol_end < -1 && line >= 0) {
		--line;
		QString context_line = doc->line (line);
		if (!prefix.isEmpty()) {   // For skipping interactive output sections in console. Empty for RKCommandEditorWindow
			if (context_line.startsWith(prefix)) {
				prefix_offset = prefix.length();
			} else if (context_line.startsWith(continuation_prefix)) {
				prefix_offset = continuation_prefix.length();
			} else {
				continue;
			}
			context_line = context_line.mid(prefix_offset);
		}
		full_context.prepend (context_line);

		int pos = context_line.length () - 1;
		if (line == cached_position.line ()) pos = cached_position.column () - 1 - prefix_offset;   // when on current line, look backward from cursor position, not line end
		for (int i = pos; i >= 0; --i) {
			QChar c = context_line.at (i);
			if (c == '(') {
				if (isCode (doc, line, i)) {
					if (--parenthesis_level < 0) {
						potential_symbol_end = i - 1;
						break;
					}
				}
			} else if (c == ')') {
				if (isCode (doc, line, i)) {
					++parenthesis_level;
				}
			}
		}
	}

	// now find out where the symbol to the left of the opening brace ends
	// there cannot be a line-break between the opening brace, and the symbol name (or can there?), so no need to fetch further context
	while ((potential_symbol_end >= 0) && full_context.at (potential_symbol_end).isSpace ()) {
		--potential_symbol_end;
	}

	// now identify the symbol and object (if any)
	RObject *object = 0;
	call_opening = KTextEditor::Cursor (-1, -1);
	if (potential_symbol_end > 0) {
		QString effective_symbol = RKCommonFunctions::getCurrentSymbol (full_context, potential_symbol_end);
		if (!effective_symbol.isEmpty ()) {
			object = RObjectList::getObjectList ()->findObject (effective_symbol);
			call_opening = KTextEditor::Cursor (line, potential_symbol_end+1);
		}
	}

	callhint_model->setFunction (object);
}

void startModel (KTextEditor::CodeCompletionInterface* iface, KTextEditor::CodeCompletionModel *model, bool start, const KTextEditor::Range &range, QList<KTextEditor::CodeCompletionModel*> *active_models) {
	if (start) {
		if (model->rowCount () == 0) start = false;
		if (!range.isValid ()) start = false;
	}
	if (start) {
		if (!active_models->contains (model)) {
			iface->startCompletion (range, model);
			active_models->append (model);
		}
	} else {
		active_models->removeAll (model);
	}
}

void RKCompletionManager::updateVisibility () {
	RK_TRACE (COMMANDEDITOR);

	if (user_triggered || !cc_iface->isCompletionActive ()) {
		active_models.clear ();
	}

	bool min_len = (currentCompletionWord ().length () >= settings->autoMinChars ()) || user_triggered;
	startModel (cc_iface, completion_model, min_len && settings->isEnabled (RKCodeCompletionSettings::Object), symbol_range, &active_models);
	startModel (cc_iface, file_completion_model, min_len && settings->isEnabled (RKCodeCompletionSettings::Filename), symbol_range, &active_models);
	if (kate_keyword_completion_model && settings->isEnabled (RKCodeCompletionSettings::AutoWord)) {
		// Model needs to update, first, as we have not handled it in tryCompletion:
		if (min_len) kate_keyword_completion_model->completionInvoked (view (), symbol_range, KTextEditor::CodeCompletionModel::ManualInvocation);
		startModel (cc_iface, kate_keyword_completion_model, min_len, symbol_range, &active_models);
	}
// NOTE: Freaky bug in KF 5.44.0: Call hint will not show for the first time, if logically above the primary screen. TODO: provide patch for kateargumenthinttree.cpp:166pp
	startModel (cc_iface, callhint_model, true && settings->isEnabled (RKCodeCompletionSettings::Calltip), currentCallRange (), &active_models);
	startModel (cc_iface, arghint_model, min_len && settings->isEnabled (RKCodeCompletionSettings::Arghint), argname_range, &active_models);

	if (active_models.isEmpty ()) {
		cc_iface->abortCompletion ();
	}
}

void RKCompletionManager::textInserted (KTextEditor::Document*, const KTextEditor::Cursor& position, const QString& text) {
	if (cc_iface->isCompletionActive ()) {
		if (position < call_opening) update_call = true;
		else if (text.contains (QChar ('(')) || text.contains (QChar (')'))) update_call = true;
	}
	tryCompletionProxy();
}

void RKCompletionManager::textRemoved (KTextEditor::Document*, const KTextEditor::Range& range, const QString& text) {
	if (cc_iface->isCompletionActive ()) {
		if (range.start () < call_opening) update_call = true;
		else if (text.contains (QChar ('(')) || text.contains (QChar (')'))) update_call = true;
	}
	tryCompletionProxy();
}

void RKCompletionManager::lineWrapped (KTextEditor::Document* , const KTextEditor::Cursor& ) {
// should already have been handled by textInserted()
	tryCompletionProxy ();
}

void RKCompletionManager::lineUnwrapped (KTextEditor::Document* , int ) {
// should already have been handled by textRemoved()
	tryCompletionProxy ();
}

void RKCompletionManager::cursorPositionChanged (KTextEditor::View* view, const KTextEditor::Cursor& newPosition) {
	if (cc_iface->isCompletionActive ()) {
		if (newPosition < call_opening) update_call = true;
		else {
			QString text = view->document ()->text (KTextEditor::Range (newPosition, cached_position));
			if (text.contains (QChar ('(')) || text.contains (QChar (')'))) update_call = true;
		}
		tryCompletionProxy ();
	} else if (settings->autoCursorActivated ()) {
		tryCompletionProxy ();
	}
}

KTextEditor::Range RKCompletionManager::currentCallRange () const {
	return KTextEditor::Range (call_opening, _view->cursorPosition ());
}

bool RKCompletionManager::eventFilter (QObject*, QEvent* event) {
	if (event->type () == QEvent::KeyPress || event->type () == QEvent::ShortcutOverride) {
		RK_TRACE (COMMANDEDITOR);	// avoid loads of empty traces, putting this here
		QKeyEvent *k = static_cast<QKeyEvent *> (event);

		if (!cc_iface->isCompletionActive()) {
			if (k->type () == QEvent::ShortcutOverride) return true; // retriggered as key event
			if (settings->tabKeyInvokesCompletion() && k->key() == Qt::Key_Tab && k->modifiers() == Qt::NoModifier) {
				userTriggeredCompletion();
				return true;
			}
			return false;
		}

		// If only the calltip is active, make sure the tab-key and enter behave as a regular keys. There is no completion in this case.
		if (active_models.count () == 1 && active_models[0] == callhint_model) {
			if (((k->key () == Qt::Key_Tab) || (k->key () == Qt::Key_Return) || (k->key () == Qt::Key_Enter)) || (k->key () == Qt::Key_Backtab)) {
				keep_active = true;
				cc_iface->abortCompletion (); // That's a bit lame, but the least hacky way to get the key into the document. keep_active=true, so
				                              // the completion window should come back up, without delay
				return false;
			}
		}

		if ((k->modifiers() == Qt::NoModifier) && ((k->key () == Qt::Key_Return) || (k->key () == Qt::Key_Enter))) {
			if (k->type () == QEvent::ShortcutOverride) {
				// Too bad for all the duplicate work, but the event will re-trigger as a keypress event, and we need to intercept that one, too.
				return true;
			}
			cc_iface->forceCompletion();
// TODO: If nothing was actually modified, should return press should be sent? Configurable?
			if (settings->autoEnabled ()) ignore_next_trigger = true;
			return true;
		}

		if (k->key () == Qt::Key_Tab && (!k->modifiers ())) {
			// Try to do partial completion. Unfortunately, the base implementation in ktexteditor is totally broken (inserts the whole partial completion, without removing the start).
			// TODO: It is not quite clear, what behavior is desirable, in case more than one completion model is active at a time.
			//       For now, we use the simplest solution (implemenation-wise), and complete from the topmost-model, only
			// TODO: Handle the ktexteditor builtin models, too.
			bool exact = false;
			QString comp;
			bool handled = false;
			if (active_models.contains (arghint_model)) {
				comp = arghint_model->partialCompletion (&exact);
				handled = true;
			} else if (active_models.contains (completion_model)) {
				comp = completion_model->partialCompletion (&exact);
				handled = true;
			} else if (active_models.contains (file_completion_model)) {
				comp = file_completion_model->partialCompletion (&exact);
				handled = true;
			}

			if (handled) {
				RK_DEBUG(COMMANDEDITOR, DL_DEBUG, "Tab completion: %s", qPrintable (comp));
				if (k->type () == QEvent::ShortcutOverride) {
					// Too bad for all the duplicate work, but the event will re-trigger as a keypress event, and we need to intercept that one, too.
					return true;
				}
				view ()->document ()->insertText (view ()->cursorPosition (), comp);
				if (exact) {
					// Ouch, how messy. We want to make sure completion stops, and is not re-triggered by the insertion, itself
					active_models.clear ();
					cc_iface->abortCompletion ();
					if (settings->autoEnabled ()) ignore_next_trigger = true;
				}
				else if (comp.isEmpty ()) {
					QApplication::beep (); // TODO: unfortunately, we catch *two* tab events, so this is not good, yet
				}
				return true;
			}
		} else if ((k->key () == Qt::Key_Up || k->key () == Qt::Key_Down) && cc_iface->isCompletionActive ()) {
			bool navigate = (settings->cursorNavigatesCompletions() && k->modifiers() == Qt::NoModifier) || (!settings->cursorNavigatesCompletions() && k->modifiers() == Qt::AltModifier);

			// Make up / down-keys (without alt) navigate in the document (aborting the completion)
			// Make alt+up / alt+down navigate in the completion list
			if (navigate) {
				if (k->type() != QKeyEvent::KeyPress) return true;  // eat the release event

				// No, we cannot just send a fake key event, easily...
				KActionCollection *kate_edit_actions = view ()->findChild<KActionCollection*> ("edit_actions");
				QAction *action = kate_edit_actions ? (kate_edit_actions->action (k->key () == Qt::Key_Up ? "move_line_up" : "move_line_down")) : 0;
				if (!action) {
					kate_edit_actions = view ()->actionCollection ();
					action = kate_edit_actions ? (kate_edit_actions->action (k->key () == Qt::Key_Up ? "move_line_up" : "move_line_down")) : 0;
				}
				if (action) action->trigger ();
				else RK_ASSERT (action);
				return true;
			} else {
				cc_iface->abortCompletion ();
				return false;
			}
		}
	}

	return false;
}

//////////////////////// RKCompletionModelBase ////////////////////

RKCompletionModelBase::RKCompletionModelBase (RKCompletionManager *manager) : KTextEditor::CodeCompletionModel (manager) {
	RK_TRACE (COMMANDEDITOR);
	n_completions = 0;
	RKCompletionModelBase::manager = manager;
}

RKCompletionModelBase::~RKCompletionModelBase () {
	RK_TRACE (COMMANDEDITOR);
}

QModelIndex RKCompletionModelBase::index (int row, int column, const QModelIndex& parent) const {
	if (!parent.isValid ()) { // root
		if (row == 0) return createIndex (row, column, quintptr (HeaderItem));
	} else if (isHeaderItem (parent)) {
		return createIndex (row, column, quintptr (LeafItem));
	}
	return QModelIndex ();
}

QModelIndex RKCompletionModelBase::parent (const QModelIndex& index) const {
	if (index.isValid () && !isHeaderItem (index)) {
		return createIndex (0, 0, quintptr (HeaderItem));
	}
	return QModelIndex ();
}

int RKCompletionModelBase::rowCount (const QModelIndex& parent) const {
	if (!parent.isValid ()) return (n_completions ? 1 : 0); // header item, if list not empty
	if (isHeaderItem (parent)) return n_completions;
	return 0;  // no children on completion entries
}

void RKCompletionModelBase::executeCompletionItem (KTextEditor::View *view, const KTextEditor::Range & word, const QModelIndex &index) const {
// I have no idea, why this is needed at all, but sometimes (and inconsitently!) the default implementation will replace the wrong range, without this.
// Importantly, the RKFileCompletionModel replaces with the wrong range, the *second* time it gets called.
	KTextEditor::CodeCompletionModel::executeCompletionItem(view, const_cast<RKCompletionModelBase*>(this)->completionRange(view, word.end()), index);
}

//////////////////////// RKCodeCompletionModel ////////////////////

RKCodeCompletionModel::RKCodeCompletionModel (RKCompletionManager *manager) : RKCompletionModelBase (manager) {
	RK_TRACE (COMMANDEDITOR);

	setHasGroups (true);
}

RKCodeCompletionModel::~RKCodeCompletionModel () {
	RK_TRACE (COMMANDEDITOR);
}

void RKCodeCompletionModel::updateCompletionList (const QString& symbol) {
	RK_TRACE (COMMANDEDITOR);

	if (current_symbol == symbol) return;	// already up to date
	beginResetModel ();

	RObject::ObjectList matches;
	QStringList objectpath = RObject::parseObjectPath (symbol);
	if (!objectpath.isEmpty () && !objectpath[0].isEmpty ()) {  // Skip completion, if the current symbol is '""' (or another empty quote), for instance
		matches = RObjectList::getObjectList ()->findObjectsMatching (symbol);
	}

	// copy the map to two lists. For one thing, we need an int indexable storage, for another, caching this information is safer
	// in case objects are removed while the completion mode is active.
	n_completions = matches.size ();
	icons.clear ();
	icons.reserve (n_completions);
	names = RObject::getFullNames (matches, RKSettingsModuleCommandEditor::completionSettings()->options());  // NOTE: Intentionally using the script editor completion settings object, here. the completion options are shared with the console!
	for (int i = 0; i < n_completions; ++i) {
		icons.append (RKStandardIcons::iconForObject (matches[i]));
	}

	current_symbol = symbol;

	endResetModel ();
}

KTextEditor::Range RKCodeCompletionModel::completionRange (KTextEditor::View *, const KTextEditor::Cursor&) {
	return manager->currentSymbolRange ();
}

QVariant RKCodeCompletionModel::data (const QModelIndex& index, int role) const {
	if (isHeaderItem (index)) {
		if (role == Qt::DisplayRole) return i18n ("Objects on search path");
		if (role == KTextEditor::CodeCompletionModel::GroupRole) return Qt::DisplayRole;
		if (role == KTextEditor::CodeCompletionModel::InheritanceDepth) return 1;  // sort below arghint model
		return QVariant ();
	}

	int col = index.column ();
	int row = index.row ();

	if ((role == Qt::DisplayRole) || (role == KTextEditor::CodeCompletionModel::CompletionRole)) {
		if (col == KTextEditor::CodeCompletionModel::Name) {
			return (names.value (row));
		}
	} else if (role == Qt::DecorationRole) {
		if (col == KTextEditor::CodeCompletionModel::Icon) {
			return (icons.value (row));
		}
	} else if (role == KTextEditor::CodeCompletionModel::InheritanceDepth) {
		return (row);  // disable sorting
	} else if (role == KTextEditor::CodeCompletionModel::MatchQuality) {
		return (10);
	}

	return QVariant ();
}

QString findCommonCompletion (const QStringList &list, const QString &lead, bool *exact_match) {
	RK_TRACE (COMMANDEDITOR);
	RK_DEBUG(COMMANDEDITOR, DL_DEBUG, "Looking for commong completion among set of %d, starting with %s", list.size (), qPrintable (lead));

	*exact_match = true;
	QString ret;
	bool first = true;
	int lead_size = lead.count ();
	for (int i = list.size () - 1; i >= 0; --i) {
		if (!list[i].startsWith (lead)) continue;

		QString candidate = list[i].mid (lead_size);
		if (first) {
			ret = candidate;
			first = false;
		} else {
			if (ret.length () > candidate.length ()) {
				ret = ret.left (candidate.length ());
				*exact_match = false;
			}

			for (int c = 0; c < ret.length(); ++c) {
				if (ret[c] != candidate[c]) {
					*exact_match = false;
					if (!c) return QString ();

					ret = ret.left (c);
					break;
				}
			}
		}
	}

	return ret;
}

QString RKCodeCompletionModel::partialCompletion (bool* exact_match) {
	RK_TRACE (COMMANDEDITOR);

	// Here, we need to do completion on the *last* portion of the object-path, only, so we will be able to complete "obj" to "object", even if "object" is present as
	// both packageA::object and packageB::object.
	// Thus as a first step, we look up the short names. We do this, lazily, as this function is only called on demand.
	QStringList objectpath = RObject::parseObjectPath (current_symbol);
	if (objectpath.isEmpty () || objectpath[0].isEmpty ()) return (QString ());
	RObject::ObjectList matches = RObjectList::getObjectList ()->findObjectsMatching (current_symbol);

	QStringList shortnames;
	for (int i = 0; i < matches.count (); ++i) {
		shortnames.append (matches[i]->getShortName ());
	}
	QString lead = objectpath.last ();
	if (!shortnames.value (0).startsWith (lead)) lead.clear ();  // This could happen if the current path ends with '$', for instance

	return (findCommonCompletion (shortnames, lead, exact_match));
}


//////////////////////// RKCallHintModel //////////////////////////
RKCallHintModel::RKCallHintModel (RKCompletionManager* manager) : RKCompletionModelBase (manager) {
	RK_TRACE (COMMANDEDITOR);
	function = 0;
}

#include <QDesktopWidget>
// TODO: There could be more than one function by a certain name, and we could support this!
void RKCallHintModel::setFunction(RObject* _function) {
	RK_TRACE (COMMANDEDITOR);

	if (function == _function) return;
	function = _function;

	beginResetModel ();
	if (function && function->isType (RObject::Function)) {
		// initialize hint
		RFunctionObject *fo = static_cast<RFunctionObject*> (function);
		QStringList args = fo->argumentNames ();
		QStringList defs = fo->argumentDefaults ();

		name = function->getFullName ();

		formals = '(';
		formatting.clear ();
		KTextEditor::Attribute format;
//		format.setFontBold (); // NOTE: Not good. makes size mis-calculation worse.
		format.setForeground (QBrush (Qt::green));  // But turns out purple?!

		// NOTE: Unfortunately, adding new-lines within (long) formals does not work. If this issue turns out to be relevant, we'll have to resort to breaking the formals into
		// several (dummy) items.
		int pos = 1;
		for (int i = 0; i < args.size (); ++i) {
			QString pair = args[i];
			if (!defs.value(i).isEmpty ()) pair.append ('=' + defs[i]);
			formatting.append ({ pos + args[i].length (), pair.length ()-args[i].length (), format });

			if (i < (args.size () - 1)) pair.append (", ");
			formals.append (pair);

			pos = pos + pair.length ();
		}
		formals.append (')');
		n_completions = 1;
	} else {
		n_completions = 0;
	}
	endResetModel ();
}

QVariant RKCallHintModel::data (const QModelIndex& index, int role) const {
	if (isHeaderItem (index)) {
		if (role == Qt::DisplayRole) return i18n ("Function calls");  // NOTE: Header is not currently used as of KF5 5.44.0
		if (role == KTextEditor::CodeCompletionModel::GroupRole) return Qt::DisplayRole;
		return QVariant ();
	}

	int col = index.column ();
	if (role == Qt::DisplayRole) {
		if (col == KTextEditor::CodeCompletionModel::Prefix) return (name);
		if (col == KTextEditor::CodeCompletionModel::Arguments) return (formals);
		if (col == KTextEditor::CodeCompletionModel::Postfix) return ("        "); // Size is of a bit for KF5 5.44.0. Provide some padding to work around cut-off parts.
	} else if (role == KTextEditor::CodeCompletionModel::ArgumentHintDepth) {
		return 1;
	} else if (role == KTextEditor::CodeCompletionModel::CompletionRole) {
		return KTextEditor::CodeCompletionModel::Function;
	} else if (role == KTextEditor::CodeCompletionModel::HighlightingMethod) {
		if (col == KTextEditor::CodeCompletionModel::Arguments) return KTextEditor::CodeCompletionModel::CustomHighlighting;
	} else if (role == KTextEditor::CodeCompletionModel::CustomHighlight) {
		if (col == KTextEditor::CodeCompletionModel::Arguments)  return formatting;
	} else if (role == KTextEditor::CodeCompletionModel::MatchQuality) {
		return (10);
	}

	return QVariant ();
}

KTextEditor::Range RKCallHintModel::completionRange (KTextEditor::View *, const KTextEditor::Cursor&) {
	return manager->currentCallRange ();
}

//////////////////////// RKArgumentHintModel //////////////////////
RKArgumentHintModel::RKArgumentHintModel (RKCompletionManager* manager) : RKCompletionModelBase (manager) {
	RK_TRACE (COMMANDEDITOR);

	function = 0;
}

void RKArgumentHintModel::updateCompletionList (RObject* _function, const QString &argument) {
	RK_TRACE (COMMANDEDITOR);

	bool changed = false;
	if (function != _function) {
		beginResetModel ();
		changed = true;
		function = _function;
		if (function && function->isType (RObject::Function)) {
			// initialize hint
			RFunctionObject *fo = static_cast<RFunctionObject*> (function);
			args = fo->argumentNames ();
			defs = fo->argumentDefaults ();
		} else {
			args.clear ();
			defs.clear ();
		}
	}

	if (changed || (argument != fragment)) {
		if (!changed) {
			changed = true;
			beginResetModel ();
		}
		fragment = argument;
		matches.clear ();
		for (int i = 0; i < args.size (); ++i) {
			if (args[i].startsWith (fragment)) matches.append (i);
		}
	}

	if (changed) {
		n_completions = matches.size ();
		endResetModel ();
	}
}

QVariant RKArgumentHintModel::data (const QModelIndex& index, int role) const {
	if (isHeaderItem (index)) {
		if (role == Qt::DisplayRole) return i18n ("Function arguments");
		if (role == KTextEditor::CodeCompletionModel::GroupRole) return Qt::DisplayRole;
		if (role == KTextEditor::CodeCompletionModel::InheritanceDepth) return 0; // Sort above other models (except calltip)
		return QVariant ();
	}

	int col = index.column ();
	int row = index.row ();
	if (role == Qt::DisplayRole) {
		if (col == KTextEditor::CodeCompletionModel::Name) return (args.value (matches.value (row)));
		if (col == KTextEditor::CodeCompletionModel::Postfix) {
			QString def = defs.value (matches.value (row));
			if (!def.isEmpty ()) return (QString ('=' + def));
		}
	} else if (role == KTextEditor::CodeCompletionModel::InheritanceDepth) {
		return row;  // disable sorting
	} else if (role == KTextEditor::CodeCompletionModel::CompletionRole) {
		return KTextEditor::CodeCompletionModel::Function;
	} else if (role == KTextEditor::CodeCompletionModel::MatchQuality) {
		return (20);
	}

	return QVariant ();
}

KTextEditor::Range RKArgumentHintModel::completionRange (KTextEditor::View*, const KTextEditor::Cursor&) {
	return manager->currentArgnameRange ();
}

QString RKArgumentHintModel::partialCompletion (bool* exact) {
	RK_TRACE (COMMANDEDITOR);

	return (findCommonCompletion (args, fragment, exact));
}

//////////////////////// RKFileCompletionModel ////////////////////
#include <KUrlCompletion>
RKFileCompletionModelWorker::RKFileCompletionModelWorker (const QString &_string) : QThread () {
	RK_TRACE (COMMANDEDITOR);
	string = _string;
	connect (this, &QThread::finished, this, &QObject::deleteLater);
}

void RKFileCompletionModelWorker::run () {
	RK_TRACE (COMMANDEDITOR);

	KUrlCompletion comp;
	comp.setDir (QUrl::fromLocalFile (QDir::currentPath ()));
	comp.makeCompletion (string);
	while (comp.isRunning()) {
		msleep(50);
	}
	QStringList files = comp.allMatches ();

	QStringList exes;
	if (!QDir (string).isAbsolute ()) {  // NOTE: KUrlCompletion does not handle this well, returns, e.g. "/home/me/firefox" when given "/home/me/f"; KF5 5.44.0
		comp.setMode (KUrlCompletion::ExeCompletion);
		comp.makeCompletion (string);
		exes = comp.allMatches ();
	}

	emit (completionsReady (string, exes, files));
}

RKFileCompletionModel::RKFileCompletionModel (RKCompletionManager* manager) : RKCompletionModelBase (manager) {
	RK_TRACE (COMMANDEDITOR);
	worker = 0;
}

RKFileCompletionModel::~RKFileCompletionModel () {
	RK_TRACE (COMMANDEDITOR);
}

void RKFileCompletionModel::updateCompletionList(const QString& fragment) {
	RK_TRACE (COMMANDEDITOR);

	if (current_fragment == fragment) return;

	current_fragment = fragment;
	launchThread ();
}

void RKFileCompletionModel::launchThread () {
	RK_TRACE (COMMANDEDITOR);

	if (current_fragment.isEmpty ()) {
		completionsReady (QString (), QStringList (), QStringList ());
	} else if (!worker) {
		RK_DEBUG (COMMANDEDITOR, DL_DEBUG, "Launching filename completion thread for '%s'", qPrintable (current_fragment));
		worker = new RKFileCompletionModelWorker (current_fragment);
		connect (worker, &RKFileCompletionModelWorker::completionsReady, this, &RKFileCompletionModel::completionsReady);
		worker->start ();
	}
}

void RKFileCompletionModel::completionsReady (const QString& string, const QStringList& exes, const QStringList& files) {
	RK_TRACE (COMMANDEDITOR);

	RK_DEBUG (COMMANDEDITOR, DL_DEBUG, "Filename completion finished for '%s': %d matches", qPrintable (string), files.size () + exes.size ());
	worker = 0;
	if (current_fragment == string) {
		beginResetModel ();
		names = files + exes; // TODO: This could be prettier
		n_completions = names.size ();
		endResetModel ();
	} else {
		launchThread ();
	}
}

KTextEditor::Range RKFileCompletionModel::completionRange (KTextEditor::View *, const KTextEditor::Cursor &) {
	return manager->currentSymbolRange ();
}

QVariant RKFileCompletionModel::data (const QModelIndex& index, int role) const {
	if (isHeaderItem (index)) {
		if (role == Qt::DisplayRole) return i18n ("Local file names");
		if (role == KTextEditor::CodeCompletionModel::GroupRole) return Qt::DisplayRole;
		if (role == KTextEditor::CodeCompletionModel::InheritanceDepth) return 1; // Sort below arghint model
		return QVariant ();
	}

	int col = index.column ();
	int row = index.row ();
	if ((role == Qt::DisplayRole) || (role == KTextEditor::CodeCompletionModel::CompletionRole)) {
		if (col == KTextEditor::CodeCompletionModel::Name) {
			return (names.value (row));
		}
	} else if (role == KTextEditor::CodeCompletionModel::InheritanceDepth) {
		return (row);  // disable sorting
	} else if (role == KTextEditor::CodeCompletionModel::MatchQuality) {
		return (10);
	}
	return QVariant ();
}

QString RKFileCompletionModel::partialCompletion (bool* exact) {
	RK_TRACE (COMMANDEDITOR);

	return (findCommonCompletion (names, current_fragment, exact));
}
