/*
 * Copyright 2026 Axel Waggershauser
 */
// SPDX-License-Identifier: Apache-2.0

#include "ZXingQt.h"

#include <QApplication>
#include <QCamera>
#include <QCameraDevice>
#include <QCheckBox>
#include <QClipboard>
#include <QComboBox>
#include <QDateTime>
#include <QDebug>
#include <QDir>
#include <QElapsedTimer>
#include <QFileDialog>
#include <QFileInfo>
#include <QGridLayout>
#include <QHBoxLayout>
#include <QImageWriter>
#include <QKeyEvent>
#include <QKeySequence>
#include <QLabel>
#include <QMainWindow>
#include <QMediaCaptureSession>
#include <QMediaDevices>
#include <QPainter>
#include <QPushButton>
#include <QTimer>
#include <QVBoxLayout>
#include <QVideoFrame>
#include <QVideoSink>
#include <QWidget>

#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)
#include <QPermission>
#endif

using namespace ZXingQt;

class VideoWidget : public QWidget
{
	Q_OBJECT

public:
	VideoWidget(QWidget* parent = nullptr) : QWidget(parent)
	{
		setAttribute(Qt::WA_OpaquePaintEvent);
		setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
	}

	void setVideoFrame(const QVideoFrame& frame)
	{
		_frame = frame;
		update();
	}

	void setBarcodes(const QList<Barcode>& barcodes)
	{
		_barcodes = barcodes;
		update();
	}

	QImage currentImage() const { return _frame.toImage(); }

protected:
	void paintEvent(QPaintEvent*) override
	{
		QPainter painter(this);
		painter.fillRect(rect(), Qt::black);

		if (!_frame.isValid())
			return;

		auto image = _frame.toImage();
		if (image.isNull())
			return;

		// Scale image to fit widget while maintaining aspect ratio
		QSize imageSize = image.size();
		imageSize.scale(size(), Qt::KeepAspectRatio);

		int x = (width() - imageSize.width()) / 2;
		int y = (height() - imageSize.height()) / 2;

		QRect targetRect(x, y, imageSize.width(), imageSize.height());
		painter.drawImage(targetRect, image);

		// Draw all barcode positions
		if (!_barcodes.isEmpty() && imageSize.width() > 0) {
			double scaleX = static_cast<double>(imageSize.width()) / image.width();
			double scaleY = static_cast<double>(imageSize.height()) / image.height();

			auto mapPoint = [&](const QPoint& p) { return QPointF(x + p.x() * scaleX, y + p.y() * scaleY); };

			QFont font = painter.font();
			QFontMetrics fm(font);
			font.setPointSize(16);
			font.setBold(true);
			painter.setFont(font);

			int barcodeIndex = 1;
			for (const auto& barcode : _barcodes) {
				const auto& position = barcode.position();
				painter.setPen(QPen(barcode.isValid() ? Qt::green : Qt::red, 3));
				for (int i = 0; i < 4; ++i)
					painter.drawLine(mapPoint(position[i]), mapPoint(position[(i + 1) % 4]));

				if (_barcodes.size() == 1)
					break;

				// Draw barcode index at center
				QString indexText = QString::number(barcodeIndex);
				QRect textRect = fm.boundingRect(indexText);
				textRect.moveCenter(mapPoint(position.center()).toPoint());
				textRect.adjust(-5, -5, 5, 5);

				painter.fillRect(textRect, QColor(128, 128, 128, 180));
				painter.setPen(Qt::white);
				painter.drawText(textRect, Qt::AlignCenter, indexText);

				++barcodeIndex;
			}
		}
	}

private:
	QVideoFrame _frame;
	QList<Barcode> _barcodes;
};

class CameraReaderWidget : public QMainWindow
{
	Q_OBJECT

public:
	CameraReaderWidget(QWidget* parent = nullptr) : QMainWindow(parent)
	{
		setWindowTitle(QStringLiteral("ZXingQtCamReader"));
		resize(640, 480);

		setupUI();
		setupCameraAndReader();
	}

	~CameraReaderWidget() override { _camera->stop(); }

private:
	void setInfoText(const QString& text)
	{
		_infoLabel->setText(text);
		_infoLabel->adjustSize();
	}

	void resizeEvent(QResizeEvent* event) override
	{
		QMainWindow::resizeEvent(event);
		if (_infoLabel)
			_infoLabel->setMaximumWidth(width());
	}

	void keyPressEvent(QKeyEvent* event) override
	{
		if (event->matches(QKeySequence::Save)) {
			saveCurrentFrame();
			event->accept();
		} else if (event->key() == Qt::Key_Space || event->key() == Qt::Key_P) {
			togglePause();
			event->accept();
		} else if (event->key() == Qt::Key_S) {
			toggleSettings();
			event->accept();
		} else {
			QMainWindow::keyPressEvent(event);
		}
	}

	void setupUI()
	{
		auto centralWidget = new QWidget(this);
		setCentralWidget(centralWidget);

		auto mainLayout = new QVBoxLayout(centralWidget);
		mainLayout->setContentsMargins(0, 0, 0, 0);
		mainLayout->setSpacing(0);

		// Camera selection (only shown when multiple cameras available)
		const auto cameras = QMediaDevices::videoInputs();
		if (cameras.size() > 1) {
			auto cameraLayout = new QHBoxLayout();
			cameraLayout->addWidget(new QLabel(tr("Camera: ")));

			_cameraCombo = new QComboBox();
			for (const auto& camera : cameras)
				_cameraCombo->addItem(camera.description(), QVariant::fromValue(camera));
			connect(_cameraCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &CameraReaderWidget::onCameraChanged);
			cameraLayout->addWidget(_cameraCombo);
			cameraLayout->addStretch();

			mainLayout->addLayout(cameraLayout);
		}

		// Video display with overlays
		_videoWidget = new VideoWidget();

		// Create a container for video widget with overlays using a grid layout
		auto videoContainer = new QWidget();
		auto gridLayout = new QGridLayout(videoContainer);
		gridLayout->setContentsMargins(0, 0, 0, 0);
		gridLayout->addWidget(_videoWidget, 0, 0, 1, 1);

		mainLayout->addWidget(videoContainer, 1);

		// Info label overlay (top-left aligned)
		_infoLabel = new QLabel(videoContainer);
		_infoLabel->setStyleSheet(QStringLiteral(
			"QLabel { color: white; background-color: rgba(40, 40, 40, 200); padding: 12px 16px; border-radius: 8px; }"));
		_infoLabel->setAttribute(Qt::WA_TransparentForMouseEvents);
		_infoLabel->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored);
		setInfoText(tr("Initializing camera..."));
		gridLayout->addWidget(_infoLabel, 0, 0, 1, 1, Qt::AlignTop | Qt::AlignLeft);

		// Feedback label overlay (center aligned)
		_feedbackLabel = new QLabel(videoContainer);
		_feedbackLabel->setStyleSheet(QStringLiteral(
			"QLabel { color: white; background-color: rgba(40, 40, 40, 200); padding: 12px 16px; border-radius: 8px; }"));
		_feedbackLabel->setAttribute(Qt::WA_TransparentForMouseEvents);
		_feedbackLabel->hide();
		gridLayout->addWidget(_feedbackLabel, 0, 0, 1, 1, Qt::AlignCenter);

		// Control buttons container (bottom-right aligned)
		auto buttonContainer = new QWidget(videoContainer);
		auto buttonLayout = new QHBoxLayout(buttonContainer);
		buttonLayout->setContentsMargins(0, 0, 0, 0);
		buttonLayout->setSpacing(8);

		const QString buttonStyle = QStringLiteral("QPushButton { "
												   "  background-color: rgba(40, 40, 40, 200); "
												   "  color: white; "
												   "  border: none; "
												   "  border-radius: 24px; "
												   "  font-size: 24px; "
												   "} "
												   "QPushButton:hover { background-color: rgba(60, 60, 60, 220); } "
												   "QPushButton:pressed { background-color: rgba(80, 80, 80, 240); }");

		// Pause/Resume button
		_pauseButton = new QPushButton(QStringLiteral("⏸"), buttonContainer);
		_pauseButton->setFixedSize(48, 48);
		_pauseButton->setStyleSheet(buttonStyle);
		_pauseButton->setCursor(Qt::PointingHandCursor);
		_pauseButton->setToolTip(tr("Pause capture"));
		connect(_pauseButton, &QPushButton::clicked, this, &CameraReaderWidget::togglePause);
		buttonLayout->addWidget(_pauseButton);

		// Settings button
		_settingsButton = new QPushButton(QStringLiteral("⚙"), buttonContainer);
		_settingsButton->setFixedSize(48, 48);
		_settingsButton->setStyleSheet(buttonStyle);
		_settingsButton->setCursor(Qt::PointingHandCursor);
		_settingsButton->setToolTip(tr("Settings"));
		connect(_settingsButton, &QPushButton::clicked, this, &CameraReaderWidget::toggleSettings);
		buttonLayout->addWidget(_settingsButton);

		gridLayout->addWidget(buttonContainer, 0, 0, 1, 1, Qt::AlignBottom | Qt::AlignRight);

		// Controls panel (initially hidden)
		_controlsWidget = new QWidget(videoContainer);
		_controlsWidget->setStyleSheet(QStringLiteral("QWidget#controlsWidget { "
													  "  background-color: rgba(40, 40, 40, 230); "
													  "  border-radius: 12px; "
													  "} "
													  "QCheckBox, QLabel { "
													  "  color: white; "
													  "  background: transparent; "
													  "  font-size: 13px; "
													  "}"));
		_controlsWidget->setObjectName(QStringLiteral("controlsWidget"));
		_controlsWidget->hide();
		auto controlsLayout = new QVBoxLayout(_controlsWidget);
		controlsLayout->setContentsMargins(16, 16, 16, 16);
		controlsLayout->setSpacing(8);

		// Header with close button
		auto headerLayout = new QHBoxLayout();
		headerLayout->addWidget(new QLabel(tr("Settings")));
		headerLayout->addStretch();

		auto closeButton = new QPushButton(QStringLiteral("✕"));
		closeButton->setFixedSize(24, 24);
		closeButton->setStyleSheet(
			QStringLiteral("QPushButton { background: transparent; border: none; color: white; font-size: 18px; }"));
		closeButton->setCursor(Qt::PointingHandCursor);
		connect(closeButton, &QPushButton::clicked, _controlsWidget, &QWidget::hide);
		headerLayout->addWidget(closeButton);
		controlsLayout->addLayout(headerLayout);

		auto addCheckBox = [&](QCheckBox*& checkbox, const QString& text, bool defaultValue = true) {
			checkbox = new QCheckBox(text);
			checkbox->setChecked(defaultValue);
			connect(checkbox, &QCheckBox::toggled, this, &CameraReaderWidget::updateReaderOptions);
			controlsLayout->addWidget(checkbox);
		};

		addCheckBox(_autoPauseCheck, tr("Auto Pause"), false);
		addCheckBox(_tryRotateCheck, tr("Try Rotate"));
		addCheckBox(_tryHarderCheck, tr("Try Harder"));
		addCheckBox(_tryInvertCheck, tr("Try Invert"));
		addCheckBox(_tryDownscaleCheck, tr("Try Downscale"));
		addCheckBox(_returnErrorsCheck, tr("Return Errors"), false);

		// Format filter combobox
		_formatCombo = new QComboBox();
		for (auto format : ListBarcodeFormats())
			_formatCombo->addItem(ToString(format), static_cast<unsigned int>(format));

		connect(_formatCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &CameraReaderWidget::updateReaderOptions);
		controlsLayout->addWidget(_formatCombo);

		// Position controls panel above the settings button with some margin
		gridLayout->addWidget(_controlsWidget, 0, 0, 1, 1, Qt::AlignBottom | Qt::AlignRight);
	}

	void toggleSettings()
	{
		if (_controlsWidget->isVisible()) {
			_controlsWidget->hide();
		} else {
			_controlsWidget->show();
			_controlsWidget->adjustSize();
		}
	}

	void togglePause() { _isPaused ? resumeScan() : pauseScan(); }

	void saveCurrentFrame()
	{
		const QImage image = _videoWidget->currentImage();
		if (image.isNull()) {
			showFeedback(tr("No video frame available to save."));
			return;
		}

		QString selectedFilter = tr("WebP image (*.webp)");
		const QString defaultPath = QStringLiteral("%1/frame-%2.webp")
			.arg(QDir::homePath(), QDateTime::currentDateTime().toString(QStringLiteral("yyyyMMdd-HHmmss")));
		const QString filePath = QFileDialog::getSaveFileName(
			this, tr("Save Current Frame"), defaultPath,
			tr("WebP image (*.webp);;PNG image (*.png);;JPEG image (*.jpg *.jpeg);;BMP image (*.bmp)"), &selectedFilter);
		if (filePath.isEmpty())
			return;

		QByteArray format = QFileInfo(filePath).suffix().toLatin1().toLower();
		if (format.isEmpty())
			format = "webp";

		QImageWriter writer(filePath, format);
		writer.setOptimizedWrite(true);
		writer.setQuality(100);

		if (!writer.write(image))
			showFeedback(tr("Save failed: %1").arg(writer.errorString()));
		else
			showFeedback(tr("Saved frame to %1").arg(QDir::toNativeSeparators(filePath)));
	}

	void pauseScan()
	{
		_isPaused = true;
		_barcodeReader->setVideoSink(nullptr);
		disconnect(_videoSink, &QVideoSink::videoFrameChanged, _videoWidget, &VideoWidget::setVideoFrame);
		_pauseButton->setText(QStringLiteral("▶"));
		_pauseButton->setToolTip(tr("Resume capture"));
		_resetTimer->stop();

		if (!_lastBarcodeText.isEmpty()) {
			QApplication::beep();
			QApplication::clipboard()->setText(_lastBarcodeText);
			showFeedback(tr("Copied content of (first) barcode to clipboard."));
		}
	}

	void resumeScan()
	{
		_isPaused = false;
		_barcodeReader->setVideoSink(_videoSink);
		connect(_videoSink, &QVideoSink::videoFrameChanged, _videoWidget, &VideoWidget::setVideoFrame);
		_pauseButton->setText(QStringLiteral("⏸"));
		_pauseButton->setToolTip(tr("Pause capture"));
		_resetTimer->start();
	}

	void setupCameraAndReader()
	{
		// Create camera and video pipeline
		_camera = new QCamera(this);
		_camera->setFocusMode(QCamera::FocusModeAutoNear);

		connect(_camera, &QCamera::errorOccurred, this, [this](QCamera::Error error, const QString& errorString) {
			qWarning() << "Camera error:" << error << errorString;
			setInfoText(tr("Camera error: %1").arg(errorString));
		});

		connect(_camera, &QCamera::activeChanged, this, [](bool active) { qDebug() << "Camera active state changed:" << active; });

		_captureSession = new QMediaCaptureSession(this);
		_captureSession->setCamera(_camera);

		_videoSink = new QVideoSink(this);
		_captureSession->setVideoOutput(_videoSink);

		connect(_videoSink, &QVideoSink::videoFrameChanged, _videoWidget, &VideoWidget::setVideoFrame);

		if (QMediaDevices::videoInputs().isEmpty()) {
			setInfoText(tr("No camera found"));
			return;
		}

		_camera->setCameraDevice(QMediaDevices::videoInputs().first());

#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)
		// Request camera permission for Qt 6.6+
		QCameraPermission cameraPermission;
		switch (qApp->checkPermission(cameraPermission)) {
		case Qt::PermissionStatus::Undetermined:
			qApp->requestPermission(cameraPermission, this, [this](const QPermission& permission) {
				if (permission.status() == Qt::PermissionStatus::Granted) {
					setInfoText(tr("Starting camera..."));
					_camera->start();
				} else {
					setInfoText(tr("Camera permission denied"));
				}
			});
			break;
		case Qt::PermissionStatus::Granted:
			setInfoText(tr("Starting camera..."));
			_camera->start();
			break;
		case Qt::PermissionStatus::Denied: setInfoText(tr("Camera permission denied")); break;
		}
#else
		_camera->start();
#endif

		// Create barcode reader and configure
		_barcodeReader = new BarcodeReader(this);
		_barcodeReader->setVideoSink(_videoSink);

		connect(_barcodeReader, &BarcodeReader::foundBarcodes, this, &CameraReaderWidget::onBarcodesFound);
		connect(_barcodeReader, &BarcodeReader::foundNoBarcodes, this, &CameraReaderWidget::onFoundNoBarcodes);

		updateReaderOptions();

		_resetTimer = new QTimer(this);
		_resetTimer->setSingleShot(true);
		_resetTimer->setInterval(1000);
		connect(_resetTimer, &QTimer::timeout, [this]() { _infoLabel->clear(); });

		_feedbackTimer = new QTimer(this);
		_feedbackTimer->setSingleShot(true);
		_feedbackTimer->setInterval(2000);
		connect(_feedbackTimer, &QTimer::timeout, [this]() { _feedbackLabel->hide(); });
	}

	void updateReaderOptions()
	{
		_barcodeReader->setFormats({static_cast<BarcodeFormat>(_formatCombo->currentData().toUInt())});
		_barcodeReader->setTryRotate(_tryRotateCheck->isChecked());
		_barcodeReader->setTryHarder(_tryHarderCheck->isChecked());
		_barcodeReader->setTryInvert(_tryInvertCheck->isChecked());
		_barcodeReader->setTryDownscale(_tryDownscaleCheck->isChecked());
		_barcodeReader->setReturnErrors(_returnErrorsCheck->isChecked());
	}

private Q_SLOTS:

	void onBarcodesFound(const QList<Barcode>& barcodes)
	{
		_videoWidget->setBarcodes(barcodes);

		_lastBarcodeText = barcodes.isEmpty() ? QString() : barcodes.first().text();

		// Build info text for all barcodes
		QStringList infoParts;
		for (int i = 0; i < barcodes.size(); ++i) {
			const auto& barcode = barcodes[i];
			if (barcodes.size() > 1)
				infoParts.append(tr("[%1]").arg(i + 1));
			infoParts.append(tr("Format: %1").arg(ToString(barcode.format())));
			if (barcode.isValid())
				infoParts.append(tr("Text: %1").arg(barcode.text()));
			else
				infoParts.append(tr("Error: %1").arg(ToString(barcode.error())));
			infoParts.append(tr("Type: %1").arg(ToString(barcode.contentType())));
			infoParts.append(QStringLiteral(""));
		}
		infoParts.append(tr("Time: %1 ms").arg(_barcodeReader->runTime.loadRelaxed()));

		setInfoText(infoParts.join(QStringLiteral("\n")));
		if (!_isPaused)
			_resetTimer->start();

		if (_autoPauseCheck->isChecked() && !_isPaused)
			pauseScan();
	}

	void onFoundNoBarcodes()
	{
		_videoWidget->setBarcodes({});

		if (!_resetTimer->isActive() && !_isPaused) {
			setInfoText(tr("No barcode found (in %1 ms)").arg(_barcodeReader->runTime.loadRelaxed()));
		}
	}

	void onCameraChanged(int index)
	{
		if (index < 0)
			return;

		auto cameraDevice = _cameraCombo->itemData(index).value<QCameraDevice>();
		_camera->stop();
		_camera->setCameraDevice(cameraDevice);
		_camera->start();
	}

	void showFeedback(const QString& message)
	{
		_feedbackLabel->setText(message);
		_feedbackLabel->adjustSize();
		_feedbackLabel->show();
		_feedbackTimer->start();
	}

private:
	VideoWidget* _videoWidget = nullptr;
	QLabel* _infoLabel = nullptr;
	QLabel* _feedbackLabel = nullptr;
	QPushButton* _pauseButton = nullptr;
	QPushButton* _settingsButton = nullptr;
	QWidget* _controlsWidget = nullptr;
	QComboBox* _cameraCombo = nullptr;
	QComboBox* _formatCombo = nullptr;
	QCheckBox* _tryRotateCheck = nullptr;
	QCheckBox* _tryHarderCheck = nullptr;
	QCheckBox* _tryInvertCheck = nullptr;
	QCheckBox* _tryDownscaleCheck = nullptr;
	QCheckBox* _returnErrorsCheck = nullptr;
	QCheckBox* _autoPauseCheck = nullptr;

	QCamera* _camera = nullptr;
	QMediaCaptureSession* _captureSession = nullptr;
	QVideoSink* _videoSink = nullptr;
	BarcodeReader* _barcodeReader = nullptr;
	QTimer* _resetTimer = nullptr;
	QTimer* _feedbackTimer = nullptr;

	bool _isPaused = false;
	QString _lastBarcodeText;
};

int main(int argc, char* argv[])
{
	QApplication app(argc, argv);
	app.setApplicationName(QStringLiteral("ZXingQtCamReader"));

	CameraReaderWidget window;
	window.show();

	return app.exec();
}

#include "ZXingQtCamReader.moc"
#include "moc_ZXingQt.cpp"
