/*
 *  Kaidan - A user-friendly XMPP client for every device!
 *
 *  Copyright (C) 2016-2023 Kaidan developers and contributors
 *  (see the LICENSE file for a full list of copyright authors)
 *
 *  Kaidan 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 3 of the License, or
 *  (at your option) any later version.
 *
 *  In addition, as a special exception, the author of Kaidan gives
 *  permission to link the code of its release with the OpenSSL
 *  project's "OpenSSL" library (or with modified versions of it that
 *  use the same license as the "OpenSSL" library), and distribute the
 *  linked executables. You must obey the GNU General Public License in
 *  all respects for all of the code used other than "OpenSSL". If you
 *  modify this file, you may extend this exception to your version of
 *  the file, but you are not obligated to do so.  If you do not wish to
 *  do so, delete this exception statement from your version.
 *
 *  Kaidan 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 Kaidan.  If not, see <http://www.gnu.org/licenses/>.
 */

/*
 * Copyright 2017 QZXing authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "QrCodeVideoFrame.h"
#include <QImage>
#include <cmath>

/**
 * rectangle of the video frame which may contain a QR code
 */
struct CaptureRect
{
	CaptureRect(const QRect& captureRect, int sourceWidth, int sourceHeight)
		: isValid(captureRect.x() >= 0 && captureRect.y() >= 0 && captureRect.isValid()),
		sourceWidth(sourceWidth),
		sourceHeight(sourceHeight),
		startX(isValid ? captureRect.x() : 0),
		targetWidth(isValid ? captureRect.width() : sourceWidth),
		endX(startX + targetWidth),
		startY(isValid ? captureRect.y() : 0),
		targetHeight(isValid ? captureRect.height() : sourceHeight),
		endY(startY + targetHeight)
	{}

	bool isValid;

	int sourceWidth;
	int sourceHeight;

	int startX;
	int targetWidth;
	int endX;

	int startY;
	int targetHeight;
	int endY;
};

uchar gray(uchar r, uchar g, uchar b)
{
	return (306 * (r & 0xFF) +
			601 * (g & 0xFF) +
			117 * (b & 0xFF) +
			0x200) >> 10;
}
uchar yuvToGray(uchar Y, uchar U, uchar V)
{
	const int C = int(Y) - 16;
	const int D = int(U) - 128;
	const int E = int(V) - 128;
	return gray(
		qBound<uchar>(0, uchar((298 * C + 409 * E + 128) >> 8), 255),
		qBound<uchar>(0, uchar((298 * C - 100 * D - 208 * E + 128) >> 8), 255),
		qBound<uchar>(0, uchar((298 * C + 516 * D + 128) >> 8), 255)
	);
}

uchar yuvToGray2(uchar y, uchar u, uchar v)
{
	double rD = y + 1.4075 * (v - 128);
	double gD = y - 0.3455 * (u - 128) - (0.7169 * (v - 128));
	double bD = y + 1.7790 * (u - 128);

	return gray(
		qBound<uchar>(0, uchar(floor(rD)), 255),
		qBound<uchar>(0, uchar(floor(gD)), 255),
		qBound<uchar>(0, uchar(floor(bD)), 255)
	);
}

static QImage* rgbDataToGrayscale(
		const uchar* data,
		const CaptureRect &captureRect,
		const int alpha,
		const int red,
		const int green,
		const int blue,
		const bool isPremultiplied = false
) {
	const int stride = (alpha < 0) ? 3 : 4;

	const int endX = captureRect.sourceWidth - captureRect.startX - captureRect.targetWidth;
	const int skipX = (endX + captureRect.startX) * stride;

	QImage *image_ptr = new QImage(captureRect.targetWidth, captureRect.targetHeight, QImage::Format_Grayscale8);
	uchar* pixelInit = image_ptr->bits();
	data += (captureRect.startY * captureRect.sourceWidth + captureRect.startX) * stride;

	for (int y = 1; y <= captureRect.targetHeight; ++y) {
		// Quick fix for iOS devices. Will be handled better in the future
#ifdef Q_OS_IOS
		uchar* pixel = pixelInit + (y - 1) * captureRect.targetWidth;
#else
		uchar* pixel = pixelInit + (captureRect.targetHeight - y) * captureRect.targetWidth;
#endif

		for (int x = 0; x < captureRect.targetWidth; ++x) {
			uchar r = data[red];
			uchar g = data[green];
			uchar b = data[blue];
			if (isPremultiplied) {
				uchar a = data[alpha];
				r = uchar((uint(r) * 255) / a);
				g = uchar((uint(g) * 255) / a);
				b = uchar((uint(b) * 255) / a);
			}
			*pixel = gray(r, g, b);
			++pixel;
			data += stride;
		}
		data += skipX;
	}

	return image_ptr;
}

void QrCodeVideoFrame::setData(QVideoFrame &frame)
{
	frame.map(QAbstractVideoBuffer::ReadOnly);

	// Copy video frame bytes to this.data.
	// This is made to try to get a better performance (less memory allocation, faster unmap)
	// Any other task is performed in a QFuture task, as we want to leave the UI thread asap.
	if (m_data.size() != frame.mappedBytes()) {
		m_data.resize(frame.mappedBytes());
	}
	memcpy(m_data.data(), frame.bits(), frame.mappedBytes());
	m_size = frame.size();
	m_pixelFormat = frame.pixelFormat();

	frame.unmap();
}

QImage *QrCodeVideoFrame::toGrayscaleImage()
{
	const CaptureRect captureRect(QRect(), m_size.width(), m_size.height());
	const auto* data = reinterpret_cast<const uchar *>(m_data.constData());
	const auto *yuvPtr = reinterpret_cast<const uint32_t *>(data);
	uchar *pixel;
	int wh;
	int w_2;
	int wh_54;

	QImage *image;
	switch (m_pixelFormat) {
	case QVideoFrame::Format_ARGB32:
		image = rgbDataToGrayscale(data, captureRect, 0, 1, 2, 3);
		break;
	case QVideoFrame::Format_ARGB32_Premultiplied:
		image = rgbDataToGrayscale(data, captureRect, 0, 1, 2, 3, true);
		break;
	case QVideoFrame::Format_RGB32:
		image = rgbDataToGrayscale(data, captureRect, 0, 1, 2, 3);
		break;
	case QVideoFrame::Format_RGB24:
		image = rgbDataToGrayscale(data, captureRect, -1, 0, 1, 2);
		break;
	// TODO: QVideoFrame::Format_RGB565
	// TODO: QVideoFrame::Format_RGB555
	// TODO: QVideoFrame::Format_ARGB8565_Premultiplied
	case QVideoFrame::Format_BGRA32:
		image = rgbDataToGrayscale(data, captureRect, 3, 2, 1, 0);
		break;
	case QVideoFrame::Format_BGRA32_Premultiplied:
		image = rgbDataToGrayscale(data, captureRect, 3, 2, 1, 0, true);
		break;
	case QVideoFrame::Format_ABGR32:
		image = rgbDataToGrayscale(data, captureRect, 0, 3, 2, 1);
		break;
	case QVideoFrame::Format_BGR32:
		image = rgbDataToGrayscale(data, captureRect, 3, 2, 1, 0);
		break;
	case QVideoFrame::Format_BGR24:
		image = rgbDataToGrayscale(data, captureRect, -1, 2, 1, 0);
		break;
	case QVideoFrame::Format_BGR565:
		/// This is a forced "conversion", colors end up swapped.
		image = new QImage(data, m_size.width(), m_size.height(), QImage::Format_RGB16);
		break;
	case QVideoFrame::Format_BGR555:
		/// This is a forced "conversion", colors end up swapped.
		image = new QImage(data, m_size.width(), m_size.height(), QImage::Format_RGB555);
		break;
	// TODO: QVideoFrame::Format_BGRA5658_Premultiplied
	case QVideoFrame::Format_YUV420P:
	case QVideoFrame::Format_NV12:
		/// nv12 format, encountered on macOS
		image = new QImage(captureRect.targetWidth, captureRect.targetHeight, QImage::Format_Grayscale8);
		pixel = image->bits();
		wh = m_size.width() * m_size.height();
		w_2 = m_size.width() / 2;
		wh_54 = wh * 5 / 4;

		for (int y = captureRect.startY; y < captureRect.endY; y++) {
			const int Y_offset = y * m_size.width();
			const int y_2 = y / 2;
			const int U_offset = y_2 * w_2 + wh;
			const int V_offset = y_2 * w_2 + wh_54;
			for (int x = captureRect.startX; x < captureRect.endX; x++) {
				const int x_2 = x / 2;
				const uchar Y = data[Y_offset + x];
				const uchar U = data[U_offset + x_2];
				const uchar V = data[V_offset + x_2];
				*pixel = yuvToGray(Y, U, V);
				++pixel;
			}
		}
		break;
	case QVideoFrame::Format_NV21:
		/// nv21 format, default on android
		/// image starts with a complete Y image, which we can use directly
		image = new QImage(data, captureRect.targetWidth, captureRect.targetHeight, QImage::Format_Grayscale8);
		break;
	case QVideoFrame::Format_YUYV:
		image = new QImage(captureRect.targetWidth, captureRect.targetHeight, QImage::Format_Grayscale8);
		pixel = image->bits();

		for (int y = captureRect.startY; y < captureRect.endY; y++){
			const uint32_t *row = &yuvPtr[y*(m_size.width()/2)];
			int end = captureRect.startX/2 + (captureRect.endX - captureRect.startX)/2;
			for (int x = captureRect.startX/2; x < end; x++){
				const uint8_t *pxl = reinterpret_cast<const uint8_t *>(&row[x]);
				const uint8_t y0 = pxl[0];
				const uint8_t u  = pxl[1];
				const uint8_t v  = pxl[3];
				const uint8_t y1 = pxl[2];

				*pixel = yuvToGray2(y0, u, v);
				++pixel;
				*pixel = yuvToGray2(y1, u, v);
				++pixel;
			}
		}

		break;
	// TODO: QVideoFrame::Format_IMC*
	// TODO: QVideoFrame::Format_*YUV*
	// TODO: QVideoFrame::Format_Y*
	// TODO: QVideoFrame::Format_Jpeg (needed?)
	default:
		image = new QImage(
			data,
			m_size.width(),
			m_size.height(),
			QVideoFrame::imageFormatFromPixelFormat(m_pixelFormat)
		);
		break;
	}
	return image;
}

QByteArray QrCodeVideoFrame::data() const
{
	return m_data;
}

QSize QrCodeVideoFrame::size() const
{
	return m_size;
}

QVideoFrame::PixelFormat QrCodeVideoFrame::pixelFormat() const
{
	return m_pixelFormat;
}
