// Copyright 2016-2023 The Mumble Developers. All rights reserved.
// Use of this source code is governed by a BSD-style license
// that can be found in the LICENSE file at the root of the
// Mumble source tree or at <https://www.mumble.info/LICENSE>.

#include <QtCore/QtCore>
#include <QtGui/QtGui>
#include <QtWidgets/QMessageBox>

#include "ManualPlugin.h"
#include "ui_ManualPlugin.h"
#include "Global.h"

#include <QPointer>

#include <cmath>
#include <float.h>

#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API
#include "../../plugins/mumble_legacy_plugin.h"

static QPointer< Manual > mDlg = nullptr;
static bool bLinkable          = false;
static bool bActive            = true;

static int iAzimuth   = 0;
static int iElevation = 0;

static const QString defaultContext  = QString::fromLatin1("Mumble");
static const QString defaultIdentity = QString::fromLatin1("Agent47");

static struct {
	float avatar_pos[3];
	float avatar_front[3];
	float avatar_top[3];
	float camera_pos[3];
	float camera_front[3];
	float camera_top[3];
	std::string context;
	std::wstring identity;
} my = { { 0, 0, 0 }, { 0, 0, 0 }, { 0, 0, 0 }, { 0, 0, 0 }, { 0, 0, 0 }, { 0, 0, 0 }, std::string(), std::wstring() };

Manual::Manual(QWidget *p) : QDialog(p) {
	setupUi(this);

	qgvPosition->viewport()->installEventFilter(this);
	qgvPosition->scale(1.0f, 1.0f);
	m_qgsScene = new QGraphicsScene(QRectF(-5.0f, -5.0f, 10.0f, 10.0f), this);

	const float indicatorDiameter = 4.0f;
	QPainterPath indicator;
	// The center of the indicator's circle will represent the current position
	indicator.addEllipse(QRectF(-indicatorDiameter / 2, -indicatorDiameter / 2, indicatorDiameter, indicatorDiameter));
	// A line will indicate the indicator's orientation (azimuth)
	indicator.moveTo(0, -indicatorDiameter / 2);
	indicator.lineTo(0, -indicatorDiameter);

	m_qgiPosition = m_qgsScene->addPath(indicator);

	qgvPosition->setScene(m_qgsScene);
	qgvPosition->fitInView(-5.0f, -5.0f, 10.0f, 10.0f, Qt::KeepAspectRatio);

	qdsbX->setRange(-FLT_MAX, FLT_MAX);
	qdsbY->setRange(-FLT_MAX, FLT_MAX);
	qdsbZ->setRange(-FLT_MAX, FLT_MAX);

	qdsbX->setValue(my.avatar_pos[0]);
	qdsbY->setValue(my.avatar_pos[1]);
	qdsbZ->setValue(my.avatar_pos[2]);

	qpbActivated->setChecked(bActive);
	qpbLinked->setChecked(bLinkable);

	qsbAzimuth->setValue(iAzimuth);
	qsbElevation->setValue(iElevation);
	updateTopAndFront(iAzimuth, iElevation);

	// Set context and identity to default values in order to
	// a) make positional audio work out of the box (needs a context)
	// b) make the user aware of what each field might contain
	qleContext->setText(defaultContext);
	qleIdentity->setText(defaultIdentity);
	my.context  = defaultContext.toStdString();
	my.identity = defaultIdentity.toStdWString();

	qsbSilentUserDisplaytime->setValue(Global::get().s.manualPlugin_silentUserDisplaytime);

	updateLoopRunning.store(false);
}

void Manual::setSpeakerPositions(const QHash< unsigned int, Position2D > &positions) {
	if (mDlg) {
		QMetaObject::invokeMethod(mDlg, "on_speakerPositionUpdate", Qt::QueuedConnection,
								  Q_ARG(PositionMap, positions));
	}
}

bool Manual::eventFilter(QObject *obj, QEvent *evt) {
	if ((evt->type() == QEvent::MouseButtonPress) || (evt->type() == QEvent::MouseMove)) {
		QMouseEvent *qme = dynamic_cast< QMouseEvent * >(evt);
		if (qme) {
			if (qme->buttons() & Qt::LeftButton) {
				QPointF qpf = qgvPosition->mapToScene(qme->pos());
				qdsbX->setValue(qpf.x());
				qdsbZ->setValue(-qpf.y());
				m_qgiPosition->setPos(qpf);
			}
		}
	}
	return QDialog::eventFilter(obj, evt);
}

void Manual::changeEvent(QEvent *e) {
	QDialog::changeEvent(e);
	switch (e->type()) {
		case QEvent::LanguageChange:
			retranslateUi(this);
			break;
		default:
			break;
	}
}

void Manual::on_qpbUnhinge_pressed() {
	qpbUnhinge->setEnabled(false);
	mDlg->setParent(nullptr);
	mDlg->show();
}

void Manual::on_qpbLinked_clicked(bool b) {
	bLinkable = b;
}

void Manual::on_qpbActivated_clicked(bool b) {
	bActive = b;
}

void Manual::on_qdsbX_valueChanged(double d) {
	my.avatar_pos[0] = my.camera_pos[0] = static_cast< float >(d);
	m_qgiPosition->setPos(my.avatar_pos[0], -my.avatar_pos[2]);
}

void Manual::on_qdsbY_valueChanged(double d) {
	my.avatar_pos[1] = my.camera_pos[1] = static_cast< float >(d);
}

void Manual::on_qdsbZ_valueChanged(double d) {
	my.avatar_pos[2] = my.camera_pos[2] = static_cast< float >(d);
	m_qgiPosition->setPos(my.avatar_pos[0], -my.avatar_pos[2]);
}

void Manual::on_qsbAzimuth_valueChanged(int i) {
	if (i > 360)
		qdAzimuth->setValue(i % 360);
	else
		qdAzimuth->setValue(i);

	updateTopAndFront(i, qsbElevation->value());
}

void Manual::on_qsbElevation_valueChanged(int i) {
	qdElevation->setValue(90 - i);
	updateTopAndFront(qsbAzimuth->value(), i);
}

void Manual::on_qdAzimuth_valueChanged(int i) {
	if (i < 0)
		qsbAzimuth->setValue(360 + i);
	else
		qsbAzimuth->setValue(i);
}

void Manual::on_qdElevation_valueChanged(int i) {
	if (i < -90)
		qdElevation->setValue(180);
	else if (i < 0)
		qdElevation->setValue(0);
	else
		qsbElevation->setValue(90 - i);
}

void Manual::on_qleContext_editingFinished() {
	my.context = qleContext->text().toStdString();
}

void Manual::on_qleIdentity_editingFinished() {
	my.identity = qleIdentity->text().toStdWString();
}

void Manual::on_buttonBox_clicked(QAbstractButton *button) {
	if (buttonBox->buttonRole(button) == buttonBox->ResetRole) {
		qpbLinked->setChecked(false);
		qpbActivated->setChecked(true);

		bLinkable = false;
		bActive   = true;

		qdsbX->setValue(0);
		qdsbY->setValue(0);
		qdsbZ->setValue(0);

		qleContext->clear();
		qleIdentity->clear();

		qsbElevation->setValue(0);
		qsbAzimuth->setValue(0);
	}
}

void Manual::on_qsbSilentUserDisplaytime_valueChanged(int value) {
	Global::get().s.manualPlugin_silentUserDisplaytime = value;
}

void Manual::on_speakerPositionUpdate(QHash< unsigned int, Position2D > positions) {
	// First iterate over the stale items to check whether one of them is actually no longer stale
	QMutableHashIterator< unsigned int, StaleEntry > staleIt(staleSpeakerPositions);
	while (staleIt.hasNext()) {
		staleIt.next();

		const unsigned int sessionID = staleIt.key();
		QGraphicsItem *staleItem     = staleIt.value().staleItem;

		if (positions.contains(sessionID)) {
			// The item is no longer stale -> restore opacity and re-insert into speakerPositions
			staleItem->setOpacity(1.0);

			staleIt.remove();
			speakerPositions.insert(sessionID, staleItem);
		} else if (!updateLoopRunning.load()) {
			QMetaObject::invokeMethod(this, "on_updateStaleSpeakers", Qt::QueuedConnection);
			updateLoopRunning.store(true);
		}
	}

	// Now iterate over all active items and check whether they have become stale or whether their
	// position can be updated
	QMutableHashIterator< unsigned int, QGraphicsItem * > speakerIt(speakerPositions);
	while (speakerIt.hasNext()) {
		speakerIt.next();

		const unsigned int sessionID = speakerIt.key();
		QGraphicsItem *speakerItem   = speakerIt.value();

		if (positions.contains(sessionID)) {
			Position2D newPos = positions.take(sessionID);

			// Update speaker's position (remember that y-axis is inverted in screen-coordinates
			speakerItem->setPos(newPos.x, -newPos.y);
		} else {
			// Remove the stale item
			speakerIt.remove();
			if (Global::get().s.manualPlugin_silentUserDisplaytime == 0) {
				// Delete it immediately
				delete speakerItem;
			} else {
				staleSpeakerPositions.insert(sessionID, { std::chrono::steady_clock::now(), speakerItem });
			}
		}
	}

	// Finally iterate over the remaining new speakers and create new items for them
	QHashIterator< unsigned int, Position2D > remainingIt(positions);
	while (remainingIt.hasNext()) {
		remainingIt.next();

		const float speakerRadius  = 1.2f;
		QGraphicsItem *speakerItem = m_qgsScene->addEllipse(-speakerRadius, -speakerRadius, 2 * speakerRadius,
															2 * speakerRadius, QPen(), QBrush(Qt::red));

		Position2D pos = remainingIt.value();

		// y-axis is inverted in screen-space
		speakerItem->setPos(pos.x, -pos.y);

		speakerPositions.insert(remainingIt.key(), speakerItem);
	}
}

void Manual::on_updateStaleSpeakers() {
	if (staleSpeakerPositions.isEmpty()) {
		// If there are no stale speakers, this loop doesn't have to run
		updateLoopRunning.store(false);
		return;
	}

	// Iterate over all stale items and check whether they have to be removed entirely. If not, update
	// their opacity.
	QMutableHashIterator< unsigned int, StaleEntry > staleIt(staleSpeakerPositions);
	while (staleIt.hasNext()) {
		staleIt.next();

		StaleEntry entry = staleIt.value();

		double elapsedTime =
			static_cast< std::chrono::duration< double > >(std::chrono::steady_clock::now() - entry.staleSince).count();

		if (elapsedTime >= Global::get().s.manualPlugin_silentUserDisplaytime) {
			// The item has been around long enough - remove it now
			staleIt.remove();
			delete entry.staleItem;
		} else {
			// Let the item fade out
			double opacity = (Global::get().s.manualPlugin_silentUserDisplaytime - elapsedTime)
							 / static_cast< double >(Global::get().s.manualPlugin_silentUserDisplaytime);
			entry.staleItem->setOpacity(opacity);
		}
	}

	if (!staleSpeakerPositions.isEmpty()) {
		updateLoopRunning.store(true);
		// Call this function again in the next iteration of the event loop
		QMetaObject::invokeMethod(this, "on_updateStaleSpeakers", Qt::QueuedConnection);
	} else {
		updateLoopRunning.store(false);
	}
}

void Manual::updateTopAndFront(int azimuth, int elevation) {
	iAzimuth   = azimuth;
	iElevation = elevation;

	m_qgiPosition->setRotation(azimuth);

	double azim = azimuth * M_PI / 180.;
	double elev = elevation * M_PI / 180.;

	my.avatar_front[0] = static_cast< float >(cos(elev) * sin(azim));
	my.avatar_front[1] = static_cast< float >(sin(elev));
	my.avatar_front[2] = static_cast< float >(cos(elev) * cos(azim));

	my.avatar_top[0] = static_cast< float >(-sin(elev) * sin(azim));
	my.avatar_top[1] = static_cast< float >(cos(elev));
	my.avatar_top[2] = static_cast< float >(-sin(elev) * cos(azim));

	memcpy(my.camera_top, my.avatar_top, sizeof(float) * 3);
	memcpy(my.camera_front, my.avatar_front, sizeof(float) * 3);
}

static int trylock() {
	return bLinkable;
}

static void unlock() {
	if (mDlg) {
		mDlg->qpbLinked->setChecked(false);
	}
	bLinkable = false;
}

static void config(void *ptr) {
	QWidget *w = reinterpret_cast< QWidget * >(ptr);

	if (mDlg) {
		mDlg->setParent(w, Qt::Dialog);
		mDlg->qpbUnhinge->setEnabled(true);
	} else {
		mDlg = new Manual(w);
	}

	mDlg->show();
}

static int fetch(float *avatar_pos, float *avatar_front, float *avatar_top, float *camera_pos, float *camera_front,
				 float *camera_top, std::string &context, std::wstring &identity) {
	if (!bLinkable)
		return false;

	if (!bActive) {
		memset(avatar_pos, 0, sizeof(float) * 3);
		memset(camera_pos, 0, sizeof(float) * 3);
		return true;
	}

	memcpy(avatar_pos, my.avatar_pos, sizeof(float) * 3);
	memcpy(avatar_front, my.avatar_front, sizeof(float) * 3);
	memcpy(avatar_top, my.avatar_top, sizeof(float) * 3);

	memcpy(camera_pos, my.camera_pos, sizeof(float) * 3);
	memcpy(camera_front, my.camera_front, sizeof(float) * 3);
	memcpy(camera_top, my.camera_top, sizeof(float) * 3);

	context.assign(my.context);
	identity.assign(my.identity);

	return true;
}

static const std::wstring longdesc() {
	return std::wstring(L"This is the manual placement plugin. It allows you to place yourself manually.");
}

static std::wstring description(L"Manual placement plugin");
static std::wstring shortname(L"Manual placement");

static void about(void *ptr) {
	QWidget *w = reinterpret_cast< QWidget * >(ptr);

	QMessageBox::about(w, QString::fromStdWString(description), QString::fromStdWString(longdesc()));
}

static MumblePlugin manual = { MUMBLE_PLUGIN_MAGIC,
							   description,
							   shortname,
							   nullptr, // About is handled by MumblePluginQt
							   nullptr, // Config is handled by MumblePluginQt
							   trylock,
							   unlock,
							   longdesc,
							   fetch };

static MumblePluginQt manualqt = { MUMBLE_PLUGIN_MAGIC_QT, about, config };

MumblePlugin *ManualPlugin_getMumblePlugin() {
	return &manual;
}

MumblePluginQt *ManualPlugin_getMumblePluginQt() {
	return &manualqt;
}


/////////// Implementation of the ManualPlugin class //////////////
ManualPlugin::ManualPlugin(QObject *p) : LegacyPlugin(QString::fromLatin1("manual.builtin"), true, p) {
}

ManualPlugin::~ManualPlugin() {
}

void ManualPlugin::resolveFunctionPointers() {
	m_mumPlug   = &manual;
	m_mumPlugQt = &manualqt;
}
