/***************************************************************************
 *   Copyright (C) 2005-2015 by the Quassel Project                        *
 *   devel@quassel-irc.org                                                 *
 *                                                                         *
 *   This class has been inspired by KDE's KKeySequenceWidget and uses     *
 *   some code snippets of its implementation, part of kdelibs.            *
 *   The original file is                                                  *
 *       Copyright (C) 1998 Mark Donohoe <donohoe@kde.org>                 *
 *       Copyright (C) 2001 Ellis Whitehead <ellis@kde.org>                *
 *       Copyright (C) 2007 Andreas Hartmetz <ahartmetz@gmail.com>         *
 *                                                                         *
 *   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.                                   *
 *                                                                         *
 *   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, write to the                         *
 *   Free Software Foundation, Inc.,                                       *
 *   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.         *
 ***************************************************************************/

#include <QApplication>
#include <QDebug>
#include <QHBoxLayout>
#include <QIcon>
#include <QKeyEvent>
#include <QMessageBox>
#include <QStyle>
#include <QToolButton>

// This defines the unicode symbols for special keys (kCommandUnicode and friends)
#ifdef Q_OS_MAC
#include <Carbon/Carbon.h>
#endif

#include "action.h"
#include "actioncollection.h"
#include "keysequencewidget.h"

KeySequenceButton::KeySequenceButton(KeySequenceWidget* d_, QWidget* parent)
	: QPushButton(parent),
	  d(d_)
{
}

bool KeySequenceButton::event(QEvent* e)
{
	if (d->isRecording() && e->type() == QEvent::KeyPress) {
		keyPressEvent(static_cast<QKeyEvent*>(e));
		return true;
	}

	// The shortcut 'alt+c' ( or any other dialog local action shortcut )
	// ended the recording and triggered the action associated with the
	// action. In case of 'alt+c' ending the dialog.  It seems that those
	// ShortcutOverride events get sent even if grabKeyboard() is active.
	if (d->isRecording() && e->type() == QEvent::ShortcutOverride) {
		e->accept();
		return true;
	}

	return QPushButton::event(e);
}

void KeySequenceButton::keyPressEvent(QKeyEvent* e)
{
	int keyQt = e->key();
	if (keyQt == -1) {
		// Qt sometimes returns garbage keycodes, I observed -1, if it doesn't know a key.
		// We cannot do anything useful with those (several keys have -1, indistinguishable)
		// and QKeySequence.toString() will also yield a garbage string.
		QMessageBox::information(this,
		                         tr("The key you just pressed is not supported by Qt."),
		                         tr("Unsupported Key"));
		return d->cancelRecording();
	}

	uint newModifiers = e->modifiers() & (Qt::SHIFT | Qt::CTRL | Qt::ALT | Qt::META);

	//don't have the return or space key appear as first key of the sequence when they
	//were pressed to start editing - catch and them and imitate their effect
	if (!d->isRecording() && ((keyQt == Qt::Key_Return || keyQt == Qt::Key_Space))) {
		d->startRecording();
		d->_modifierKeys = newModifiers;
		d->updateShortcutDisplay();
		return;
	}

	// We get events even if recording isn't active.
	if (!d->isRecording())
		return QPushButton::keyPressEvent(e);

	e->accept();
	d->_modifierKeys = newModifiers;

	switch (keyQt) {
	case Qt::Key_AltGr://or else we get unicode salad
		return;
	case Qt::Key_Shift:
	case Qt::Key_Control:
	case Qt::Key_Alt:
	case Qt::Key_Meta:
	case Qt::Key_Menu://unused (yes, but why?)
		d->updateShortcutDisplay();
		break;

	default:
		if (!(d->_modifierKeys & ~Qt::SHIFT)) {
			// It's the first key and no modifier pressed. Check if this is
			// allowed
			if (!d->isOkWhenModifierless(keyQt))
				return;
		}

		// We now have a valid key press.
		if (keyQt) {
			if ((keyQt == Qt::Key_Backtab) && (d->_modifierKeys & Qt::SHIFT)) {
				keyQt = Qt::Key_Tab | d->_modifierKeys;
			}
			else if (d->isShiftAsModifierAllowed(keyQt)) {
				keyQt |= d->_modifierKeys;
			}
			else
				keyQt |= (d->_modifierKeys & ~Qt::SHIFT);

			d->_keySequence = QKeySequence(keyQt);
			d->doneRecording();
		}
	}
}

void KeySequenceButton::keyReleaseEvent(QKeyEvent* e)
{
	if (e->key() == -1) {
		// ignore garbage, see keyPressEvent()
		return;
	}

	if (!d->isRecording())
		return QPushButton::keyReleaseEvent(e);

	e->accept();

	uint newModifiers = e->modifiers() & (Qt::SHIFT | Qt::CTRL | Qt::ALT | Qt::META);

	// if a modifier that belongs to the shortcut was released...
	if ((newModifiers & d->_modifierKeys) < d->_modifierKeys) {
		d->_modifierKeys = newModifiers;
		d->updateShortcutDisplay();
	}
}

/******************************************************************************/

KeySequenceWidget::KeySequenceWidget(QWidget* parent)
	: QWidget(parent),
	  _shortcutsModel(nullptr),
	  _isRecording(false),
	  _modifierKeys(0)
{
	QHBoxLayout* layout = new QHBoxLayout(this);
	layout->setContentsMargins(0, 0, 0, 0);

	_keyButton = new KeySequenceButton(this, this);
	_keyButton->setFocusPolicy(Qt::StrongFocus);
	_keyButton->setToolTip(tr("Click on the button, then enter the shortcut like you would in the program.\nExample for Ctrl+a: hold the Ctrl key and press a."));
	layout->addWidget(_keyButton);

	_clearButton = new QToolButton(this);
	layout->addWidget(_clearButton);

	_clearButton->setIcon(style()->standardIcon(QStyle::SP_LineEditClearButton));

	setLayout(layout);

	connect(_keyButton, SIGNAL(clicked()), SLOT(startRecording()));
	connect(_keyButton, SIGNAL(clicked()), SIGNAL(clicked()));
	connect(_clearButton, SIGNAL(clicked()), SLOT(clear()));
	connect(_clearButton, SIGNAL(clicked()), SIGNAL(clicked()));
}

void KeySequenceWidget::setModel(ShortcutsModel* model)
{
	Q_ASSERT(!_shortcutsModel);
	_shortcutsModel = model;
}

bool KeySequenceWidget::isOkWhenModifierless(int keyQt) const
{
	//this whole function is a hack, but especially the first line of code
	//    if (QKeySequence(keyQt).toString().length() == 1)
	//        return false;

	switch (keyQt) {
	case Qt::Key_Return:
		//    case Qt::Key_Space:
	case Qt::Key_Tab:
	case Qt::Key_Backtab://does this ever happen?
	case Qt::Key_Backspace:
	case Qt::Key_Delete:
		return false;
	default:
		return true;
	}
}

bool KeySequenceWidget::isShiftAsModifierAllowed(int keyQt) const
{
	// Shift only works as a modifier with certain keys. It's not possible
	// to enter the SHIFT+5 key sequence for me because this is handled as
	// '%' by qt on my keyboard.
	// The working keys are all hardcoded here :-(
	if (keyQt >= Qt::Key_F1 && keyQt <= Qt::Key_F35)
		return true;

	if (QChar(keyQt).isLetter())
		return true;

	switch (keyQt) {
	case Qt::Key_Return:
	case Qt::Key_Space:
	case Qt::Key_Backspace:
	case Qt::Key_Escape:
	case Qt::Key_Print:
	case Qt::Key_ScrollLock:
	case Qt::Key_Pause:
	case Qt::Key_PageUp:
	case Qt::Key_PageDown:
	case Qt::Key_Insert:
	case Qt::Key_Delete:
	case Qt::Key_Home:
	case Qt::Key_End:
	case Qt::Key_Up:
	case Qt::Key_Down:
	case Qt::Key_Left:
	case Qt::Key_Right:
		return true;

	default:
		return false;
	}
}

void KeySequenceWidget::updateShortcutDisplay()
{
	QString s = _keySequence.toString(QKeySequence::NativeText);
	s.replace('&', QLatin1String("&&"));

	if (_isRecording) {
		if (_modifierKeys) {
#ifdef Q_OS_MAC
			if (_modifierKeys & Qt::META) s += QChar::fromUcs2(kControlUnicode);
			if (_modifierKeys & Qt::ALT) s += QChar::fromUcs2(kOptionUnicode);
			if (_modifierKeys & Qt::SHIFT) s += QChar::fromUcs2(kShiftUnicode);
			if (_modifierKeys & Qt::CTRL) s += QChar::fromUcs2(kCommandUnicode);
#else
			if (_modifierKeys & Qt::META) s += tr("Meta", "Meta key") + '+';
			if (_modifierKeys & Qt::CTRL) s += tr("Ctrl", "Ctrl key") + '+';
			if (_modifierKeys & Qt::ALT) s += tr("Alt", "Alt key") + '+';
			if (_modifierKeys & Qt::SHIFT) s += tr("Shift", "Shift key") + '+';
#endif
		}
		else {
			s = tr("Input", "What the user inputs now will be taken as the new shortcut");
		}
		// make it clear that input is still going on
		s.append(" ...");
	}

	if (s.isEmpty()) {
		s = tr("None", "No shortcut defined");
	}

	s.prepend(' ');
	s.append(' ');
	_keyButton->setText(s);
}

void KeySequenceWidget::startRecording()
{
	_modifierKeys = 0;
	_oldKeySequence = _keySequence;
	_keySequence = QKeySequence();
	_conflictingIndex = QModelIndex();
	_isRecording = true;
	_keyButton->grabKeyboard();

	if (!QWidget::keyboardGrabber()) {
		qWarning() << "Failed to grab the keyboard! Most likely qt's nograb option is active";
	}

	_keyButton->setDown(true);
	updateShortcutDisplay();
}

void KeySequenceWidget::doneRecording()
{
	bool wasRecording = _isRecording;
	_isRecording = false;
	_keyButton->releaseKeyboard();
	_keyButton->setDown(false);

	if (!wasRecording || _keySequence == _oldKeySequence) {
		// The sequence hasn't changed
		updateShortcutDisplay();
		return;
	}

	if (!isKeySequenceAvailable(_keySequence)) {
		_keySequence = _oldKeySequence;
	}
	else if (wasRecording) {
		emit keySequenceChanged(_keySequence, _conflictingIndex);
	}
	updateShortcutDisplay();
}

void KeySequenceWidget::cancelRecording()
{
	_keySequence = _oldKeySequence;
	doneRecording();
}

void KeySequenceWidget::setKeySequence(const QKeySequence& seq)
{
	// oldKeySequence holds the key sequence before recording started, if setKeySequence()
	// is called while not recording then set oldKeySequence to the existing sequence so
	// that the keySequenceChanged() signal is emitted if the new and previous key
	// sequences are different
	if (!isRecording())
		_oldKeySequence = _keySequence;

	_keySequence = seq;
	_clearButton->setVisible(!_keySequence.isEmpty());
	doneRecording();
}

void KeySequenceWidget::clear()
{
	setKeySequence(QKeySequence());
	// setKeySequence() won't emit a signal when we're not recording
	emit keySequenceChanged(QKeySequence());
}

bool KeySequenceWidget::isKeySequenceAvailable(const QKeySequence& seq)
{
	if (seq.isEmpty())
		return true;

	// We need to access the root model, not the filtered one
	for (int cat = 0; cat < _shortcutsModel->rowCount(); cat++) {
		QModelIndex catIdx = _shortcutsModel->index(cat, 0);
		for (int r = 0; r < _shortcutsModel->rowCount(catIdx); r++) {
			QModelIndex actIdx = _shortcutsModel->index(r, 0, catIdx);
			Q_ASSERT(actIdx.isValid());
			if (actIdx.data(ShortcutsModel::ActiveShortcutRole).value<QKeySequence>() != seq)
				continue;

			if (!actIdx.data(ShortcutsModel::IsConfigurableRole).toBool()) {
				QMessageBox::warning(this, tr("Shortcut Conflict"),
				                     tr("The \"%1\" shortcut is already in use, and cannot be configured.\nPlease choose another one.").arg(seq.toString(QKeySequence::NativeText)),
				                     QMessageBox::Ok);
				return false;
			}

			QMessageBox box(QMessageBox::Warning, tr("Shortcut Conflict"),
			                (tr("The \"%1\" shortcut is ambiguous with the shortcut for the following action:")
			                 + "<br><ul><li>%2</li></ul><br>"
			                 + tr("Do you want to reassign this shortcut to the selected action?"))
			                        .arg(seq.toString(QKeySequence::NativeText), actIdx.data().toString()),
			                QMessageBox::Cancel, this);
			box.addButton(tr("Reassign"), QMessageBox::AcceptRole);
			if (box.exec() == QMessageBox::Cancel)
				return false;

			_conflictingIndex = actIdx;
			return true;
		}
	}
	return true;
}

#include "moc_keysequencewidget.cpp"
