/*
rkqwebview - This file is part of the RKWard project. Created: Sun Feb 08 2026
SPDX-FileCopyrightText: 2026 by Thomas Friedrichsmeier <thomas.friedrichsmeier@kdemail.net>
SPDX-FileContributor: The RKWard Team <rkward@kde.org>
SPDX-License-Identifier: GPL-2.0-or-later
*/

#if RK_WITH_QWEBVIEW
#	include "rkqwebview.h"

#	include <KLocalizedString>
#	include <KStandardActions>

#	include <QClipboard>
#	include <QFile>
#	include <QMenu>
#	include <QQuickItem>
#	include <QQuickWidget>
#	include <QWebSocket>
#	include <QWebSocketServer>

#	include "../misc/rkfindbar.h"
#	include "rkhtmlwindow.h"
#	include "rkworkplace.h"

#	include "../debug.h"

class RKQWebViewCallbackServer {
  public:
	static void initConnection(RKQWebView *window) {
		if (!server) {
			server = new QWebSocketServer(QString(), QWebSocketServer::NonSecureMode);
			server->listen(QHostAddress::LocalHost);
			QObject::connect(server, &QWebSocketServer::newConnection, server, []() {
				auto server = RKQWebViewCallbackServer::server;
				auto con = server->nextPendingConnection();
				const auto url = con->requestUrl().url();
				auto win = entrypoints.value(url);
				entrypoints.remove(url);
				RK_DEBUG(APP, DL_DEBUG, "New connection at %s from window %p", qPrintable(url), win);
				if (!win) {
					con->abort();
				}

				QObject::connect(con, &QWebSocket::disconnected, win, [con, win]() {
					con->deleteLater();
				});

				QObject::connect(con, &QWebSocket::textMessageReceived, win, [win](const QString &message) {
					win->receivedCallbackMessage(message);
				});
			});
		}

		// Window may already be in our map. We need to re-init on every url change
		auto key = entrypoints.key(window);
		if (key.isEmpty()) {
			key = server->serverUrl().url() + u"/"_s + QUuid::createUuid().toString(QUuid::Id128);
			entrypoints.insert(key, window);
			QObject::connect(window, &QObject::destroyed, server, [key]() {
				entrypoints.remove(key);
			});
		}

		window->runJS(u"let __rkward = new WebSocket('%1');"_s.arg(key));
	}

  protected:
	static QHash<QString, RKQWebView *> entrypoints;
	static QWebSocketServer *server;
};
QHash<QString, RKQWebView *> RKQWebViewCallbackServer::entrypoints;
QWebSocketServer *RKQWebViewCallbackServer::server = nullptr;

RKQWebView::RKQWebView(RKHTMLWindow *parent) : RKHTMLViewer(parent), view(nullptr), running_script(false), context_menu(nullptr) {
	RK_TRACE(APP);
	QFile file(u":/js/rkqwebview.js"_s);
	if (file.open(QIODevice::ReadOnly)) {
		installPersistentJS(QString::fromUtf8(file.readAll()), file.fileName());
	} else {
		RK_ASSERT(false);
	}
}

RKQWebView::~RKQWebView() {
	RK_TRACE(APP);
	delete context_menu;
}

QUrl RKQWebView::currentAcceptedUrl() const {
	return webView()->property("acceptedUrl").toUrl();
}

void RKQWebView::onUrlChanged(const QUrl &_url, const QString &error, int status) {
	// WebView status enum: 0: LoadStarted, 2: LoadFinished, 3: Error.
	// I do not actually know, what status==1 is, but it does get sent in some cases. I assume it's "aborted"
	RK_DEBUG(APP, DL_WARNING, "webview %p url changed to %s status %d, msg %s", this, qPrintable(_url.toString()), status, qPrintable(error));
	bool new_window = false;
	Q_EMIT navigationRequest(currentAcceptedUrl(), _url, new_window);
	// we may be redirected via the above signal
	if (_url != url()) {
		RK_DEBUG(APP, DL_DEBUG, "redirecting %s to %s", qPrintable(_url.toString()), qPrintable(url().toString()));
	}
	if (status == 0 && _url.scheme() == u"rkward"_s) {
		// Some QWebView plugins will also trigger openRKWardURl() via QDesktopServices::setUrlHandler()
		// Others won't. In either case, we've handled it from here.
		RKWorkplace::mainWorkplace()->suppressRKWardUrlHandling(_url);
	}
}

void RKQWebView::onLoadFinished(const QUrl &url) {
	RK_TRACE(APP);
	RK_ASSERT(currentAcceptedUrl() == url);
	selected_text.clear();
	RKQWebViewCallbackServer::initConnection(this);
	for (const auto &script : std::as_const(persistentScripts)) {
		RKHTMLViewer::runJS(script);
	}
	Q_EMIT loadFinished();
}

QWidget *RKQWebView::createWidget() {
	RK_TRACE(APP);
	view = new QQuickWidget();
	view->setResizeMode(QQuickWidget::SizeRootObjectToView);
	view->setSource(QUrl(QStringLiteral("qrc:/qml/rkqwebview.qml")));
	connect(webView(), SIGNAL(pageUrlChanged(const QUrl &, const QString &, int)), this, SLOT(onUrlChanged(const QUrl &, const QString &, int)));
	connect(webView(), SIGNAL(loadFinished(const QUrl &)), this, SLOT(onLoadFinished(const QUrl &)));
	connect(webView(), SIGNAL(runJSResult(const QVariant &)), this, SLOT(onRunJSResult(const QVariant &)));
	return view;
}

QObject *RKQWebView::webView() const {
	if (!view) {
		// could happen (happened in the past) due to premature calls to this function.
		RK_ASSERT(view);
		return nullptr;
	}
	if (view->status() != QQuickWidget::Ready) {
		RK_ASSERT(view->status() == QQuickWidget::Ready);
		return nullptr;
	}
	auto wv = view->rootObject()->findChild<QObject *>("webView");
	RK_ASSERT(wv);
	return wv;
}

QUrl RKQWebView::url() const {
	RK_TRACE(APP);
	auto wv = webView();
	return wv ? wv->property("url").toUrl() : QUrl();
}

void RKQWebView::load(const QUrl &url) {
	RK_TRACE(APP);
	webView()->setProperty("acceptedUrl", url);
	webView()->setProperty("url", url);
}

void RKQWebView::print() {
	RK_TRACE(APP);
	// TODO: Doesn't do anything (no error, either). Why?
	RKHTMLViewer::runJS(u"window.print()"_s);

	/* This is not really a useful alternative...
	QPointer<QPrintDialog> dlg(new QPrintDialog(view));
	if (dlg->exec() == QPrintDialog::Accepted) {
	    view->render(dlg->printer(), QPoint(), QRegion(), QWidget::DrawChildren);
	}
	delete dlg; */
}

void RKQWebView::installPersistentJS(const QString &script, const QString &id) {
	RK_TRACE(APP);
	persistentScripts[id] = script;
}

void RKQWebView::runJS(const QString &script, std::function<void(const QVariant &)> callback) {
	RK_TRACE(APP);
	runjs_queue.append({script, callback});
	tryRunNextScript();
}

void RKQWebView::tryRunNextScript() {
	RK_TRACE(APP);
	if (running_script) return;
	if (runjs_queue.isEmpty()) return;
	running_script = true;
	QMetaObject::invokeMethod(webView(), "runJSWrapper", Q_ARG(QString, runjs_queue.first().script));
}

void RKQWebView::onRunJSResult(const QVariant &result) {
	RK_TRACE(APP);
	RK_ASSERT(!runjs_queue.isEmpty());
	running_script = false;
	const auto callback = runjs_queue.takeFirst().callback;
	if (callback != nullptr) callback(result);
	tryRunNextScript();
}

void RKQWebView::setHTML(const QString &html, const QUrl &_url) {
	RK_TRACE(APP);
	QMetaObject::invokeMethod(webView(), "loadHtml", Q_ARG(QString, html), Q_ARG(QUrl, _url));
}

bool RKQWebView::installHelpProtocolHandler() {
	RK_TRACE(APP);
	// TODO
	return false;
}

void RKQWebView::findRequest(const QString &text, bool backwards, RKFindBar *findbar, bool *found) {
	RK_TRACE(APP);

	*found = true; // real result is not available, synchronously
	auto _txt = text;
	// NOTE: window.find() is marked as not standardized at the time of this writing!
	QString script = u"window.find('%1', %2, %3, true)"_s.arg(_txt.replace(u"'"_s, u"\\'"_s),
	                                                          (findbar->isOptionSet(RKFindBar::MatchCase) ? u"true"_s : u"false"_s),
	                                                          (backwards ? u"true"_s : u"false"_s));
	runJS(script, [findbar](const QVariant &res) {
		if (!res.toBool()) findbar->indicateSearchFail();
	});
}

QString RKQWebView::selectedText() const {
	RK_TRACE(APP);

	return selected_text;
}

void RKQWebView::receivedCallbackMessage(const QString &message) {
	RK_TRACE(APP);
	const auto mo = QJsonDocument::fromJson(message.toUtf8()).object();
	const auto msg = mo["msg"_L1].toString();
	const auto args = mo["args"_L1];
	if (msg == "selChanged"_L1) {
		const auto sel = args["sel"_L1].toString();
		if (sel != selected_text) {
			selected_text = sel;
			Q_EMIT selectionChanged(!sel.isEmpty());
		}
	} else if (msg == "scroll"_L1) {
		scroll_pos = QPoint(args["x"_L1].toInt(), args["y"_L1].toInt());
	} else if (msg == "contextMenu"_L1) {
		// TODO: some actions we probably want:
		// back, forward, reload, copy, copy link address, open in new tab, open in external browser,
		// save image as
		// TODO most to all of these actions could/should probably be handled in the RKHTMLWindow?
		if (!context_menu) {
			context_menu = new QMenu;
			auto a = new QAction(i18n("Open link in new tab"), this);
			a->setObjectName("open_new");
			connect(a, &QAction::triggered, this, [a, this]() {
				Q_EMIT navigationRequest(url(), QUrl(a->property("_url").toString()), true);
			});
			context_menu->addAction(a);

			// NOTE copy is already present as an action in the main window, but we want a separate
			// one that we can show/hide
			a = KStandardActions::copy(window, &RKHTMLWindow::slotCopy, this);
			a->setObjectName("copy");
			context_menu->addAction(a);

			a = new QAction(i18n("Copy link address"), this);
			connect(a, &QAction::triggered, this, [a]() {
				qApp->clipboard()->setText(a->property("_url").toString());
			});
			a->setObjectName("copy_link");
			context_menu->addAction(a);
		}
		const auto url = args["url"_L1].toString();
		auto a = findChild<QAction *>("open_new", Qt::FindDirectChildrenOnly);
		a->setVisible(!url.isEmpty());
		a->setProperty("_url", url);
		a = findChild<QAction *>("copy_link", Qt::FindDirectChildrenOnly);
		a->setVisible(!url.isEmpty());
		a->setProperty("_url", url);
		a = findChild<QAction *>("copy", Qt::FindDirectChildrenOnly);
		a->setVisible(!selected_text.isEmpty());
		Q_EMIT aboutToShowContextMenu(context_menu);
		context_menu->exec((QPoint(args["x"_L1].toInt(), args["y"_L1].toInt()) - scroll_pos));
	} else if (msg == "pageInternalNav"_L1) {
		Q_EMIT pageInternalNavigation(QUrl(args["href"_L1].toString()));
	} else {
		RK_DEBUG(APP, DL_ERROR, "Non-recognized message %s", qPrintable(message));
	}
}

void RKQWebView::exportPage() {
	RK_TRACE(APP);
	// TODO
}

QPoint RKQWebView::scrollPosition() const {
	RK_TRACE(APP);
	return scroll_pos;
}

void RKQWebView::setScrollPosition(const QPoint &pos, bool wait_for_load) {
	RK_TRACE(APP);
	runJS(u"window.scroll({top: %1, left: %2, behavior: 'instant'});"_s.arg(pos.y(), pos.x()));
}

bool RKQWebView::supportsContentType(const QString &mimename) {
	RK_TRACE(APP);
	// TODO
	return true;
}

void RKQWebView::zoomIn() {
	RK_TRACE(APP);
	QMetaObject::invokeMethod(webView(), "doZoom", Q_ARG(int, 1));
}

void RKQWebView::zoomOut() {
	RK_TRACE(APP);
	QMetaObject::invokeMethod(webView(), "doZoom", Q_ARG(int, -1));
}
#endif
