/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.

For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "media/view/media_view_overlay_opengl.h"

#include "data/data_peer_values.h" // AmPremiumValue.
#include "ui/gl/gl_shader.h"
#include "ui/painter.h"
#include "media/stories/media_stories_view.h"
#include "media/streaming/media_streaming_common.h"
#include "media/view/media_view_video_stream.h"
#include "platform/platform_overlay_widget.h"
#include "base/platform/base_platform_info.h"
#include "core/crash_reports.h"
#include "styles/style_media_view.h"

namespace Media::View {
namespace {

using namespace Ui::GL;

constexpr auto kNotchOffset = 4;
constexpr auto kRadialLoadingOffset = kNotchOffset + 4;
constexpr auto kThemePreviewOffset = kRadialLoadingOffset + 4;
constexpr auto kDocumentBubbleOffset = kThemePreviewOffset + 4;
constexpr auto kSaveMsgOffset = kDocumentBubbleOffset + 4;
constexpr auto kChapterOffset = kSaveMsgOffset + 4;
constexpr auto kSpeedBoostOffset = kChapterOffset + 4;
constexpr auto kFooterOffset = kSpeedBoostOffset + 4;
constexpr auto kCaptionOffset = kFooterOffset + 4;
constexpr auto kGroupThumbsOffset = kCaptionOffset + 4;
constexpr auto kControlsOffset = kGroupThumbsOffset + 4;
constexpr auto kControlValues = 4 * 4 + 4 * 4; // over + icon

[[nodiscard]] ShaderPart FragmentApplyControlsFade() {
	return {
		.header = R"(
uniform sampler2D f_texture;
uniform vec4 shadowTopRect;
uniform vec4 shadowBottomSkipOpacityFullFade;
)",
		.body = R"(
	float topHeight = shadowTopRect.w;
	float bottomHeight = shadowBottomSkipOpacityFullFade.x;
	float bottomSkip = shadowBottomSkipOpacityFullFade.y;
	float opacity = shadowBottomSkipOpacityFullFade.z;
	float fullFade = shadowBottomSkipOpacityFullFade.w;
	float viewportHeight = shadowTopRect.y + topHeight;
	float fullHeight = topHeight + bottomHeight;
	float topY = min(
		(viewportHeight - gl_FragCoord.y) / fullHeight,
		topHeight / fullHeight);
	float topX = (gl_FragCoord.x - shadowTopRect.x) / shadowTopRect.z;
	vec4 fadeTop = texture2D(f_texture, vec2(topX, topY)) * opacity;
	float bottomY = max(bottomSkip + fullHeight - gl_FragCoord.y, topHeight)
		/ fullHeight;
	vec4 fadeBottom = texture2D(f_texture, vec2(0.5, bottomY)) * opacity;
	float fade = min((1. - fadeTop.a) * (1. - fadeBottom.a), fullFade);
	result.rgb = result.rgb * fade;
)",
	};
}

[[nodiscard]] ShaderPart FragmentPlaceOnTransparentBackground() {
	return {
		.header = R"(
uniform vec4 transparentBg;
uniform vec4 transparentFg;
uniform float transparentSize;
)",
		.body = R"(
	vec2 checkboardLadder = floor(gl_FragCoord.xy / transparentSize);
	float checkboard = mod(checkboardLadder.x + checkboardLadder.y, 2.0);
	vec4 checkboardColor = checkboard * transparentBg
		+ (1. - checkboard) * transparentFg;
	result += checkboardColor * (1. - result.a);
)",
	};
}

[[nodiscard]] ShaderPart FragmentRoundedCorners() {
	return {
		.header = R"(
uniform vec4 roundRect;
uniform float roundRadius;

float roundedCorner() {
	vec2 rectHalf = roundRect.zw / 2.;
	vec2 rectCenter = roundRect.xy + rectHalf;
	vec2 fromRectCenter = abs(gl_FragCoord.xy - rectCenter);
	vec2 vectorRadius = vec2(roundRadius + 0.5, roundRadius + 0.5);
	vec2 fromCenterWithRadius = fromRectCenter + vectorRadius;
	vec2 fromRoundingCenter = max(fromCenterWithRadius, rectHalf)
		- rectHalf;
	float rounded = length(fromRoundingCenter) - roundRadius;

	return 1. - smoothstep(0., 1., rounded);
}
)",
		.body = R"(
	result *= roundedCorner();
)",
	};
}

[[nodiscard]] QRectF StoryCropTextureRect(
		QSizeF imageSize,
		QSizeF targetSize) {
	if (imageSize.isEmpty() || targetSize.isEmpty()) {
		return QRectF(0., 0., 1., 1.);
	}
	const auto targetAspect = targetSize.width() / targetSize.height();
	const auto imageAspect = imageSize.width() / imageSize.height();
	if (imageAspect > targetAspect) {
		const auto cropW = imageSize.height() * targetAspect;
		const auto offset = (imageSize.width() - cropW) / 2.;
		return QRectF(
			offset / imageSize.width(),
			0.,
			cropW / imageSize.width(),
			1.);
	} else if (imageAspect < targetAspect) {
		const auto cropH = imageSize.width() / targetAspect;
		const auto offset = (imageSize.height() - cropH) / 2.;
		return QRectF(
			0.,
			offset / imageSize.height(),
			1.,
			cropH / imageSize.height());
	}
	return QRectF(0., 0., 1., 1.);
}

} // namespace

OverlayWidget::RendererGL::RendererGL(not_null<OverlayWidget*> owner)
: _owner(owner) {
	style::PaletteChanged(
	) | rpl::on_next([=] {
		_controlsFadeImage.invalidate();
		_radialImage.invalidate();
		_documentBubbleImage.invalidate();
		_themePreviewImage.invalidate();
		_saveMsgImage.invalidate();
		_footerImage.invalidate();
		_captionImage.invalidate();
		invalidateControls();
	}, _lifetime);

	crl::on_main(this, [=] {
		_owner->_storiesChanged.events(
		) | rpl::on_next([=] {
			if (_owner->_storiesSession) {
				Data::AmPremiumValue(
					_owner->_storiesSession
				) | rpl::on_next([=] {
					invalidateControls();
				}, _storiesLifetime);
			} else {
				_storiesLifetime.destroy();
				invalidateControls();
			}
		}, _lifetime);
	});
}

void OverlayWidget::RendererGL::init(QOpenGLFunctions &f) {
	constexpr auto kQuads = 9;
	constexpr auto kQuadVertices = kQuads * 4;
	constexpr auto kQuadValues = kQuadVertices * 4;
	constexpr auto kControlsValues = kControlsCount * kControlValues;
	constexpr auto kRoundingQuads = 4;
	constexpr auto kRoundingVertices = kRoundingQuads * 6;
	constexpr auto kRoundingValues = kRoundingVertices * 2;
	constexpr auto kStoriesSiblingValues = kStoriesSiblingPartsCount * 16;
	constexpr auto kValues = kQuadValues
		+ kControlsValues
		+ kRoundingValues
		+ kStoriesSiblingValues;

	_contentBuffer.emplace();
	_contentBuffer->setUsagePattern(QOpenGLBuffer::DynamicDraw);
	_contentBuffer->create();
	_contentBuffer->bind();
	_contentBuffer->allocate(kValues * sizeof(GLfloat));

	_textures.ensureCreated(f);

	_imageProgram.emplace();
	_texturedVertexShader = LinkProgram(
		&*_imageProgram,
		VertexShader({
			VertexViewportTransform(),
			VertexPassTextureCoord(),
		}),
		FragmentShader({
			FragmentSampleARGB32Texture(),
		})).vertex;

	_staticContentProgram.emplace();
	LinkProgram(
		&*_staticContentProgram,
		_texturedVertexShader,
		FragmentShader({
			FragmentSampleARGB32Texture(),
			FragmentApplyControlsFade(),
			FragmentRoundedCorners()
		}));

	_withTransparencyProgram.emplace();
	LinkProgram(
		&*_withTransparencyProgram,
		_texturedVertexShader,
		FragmentShader({
			FragmentSampleARGB32Texture(),
			FragmentPlaceOnTransparentBackground(),
			FragmentApplyControlsFade()
		}));

	_yuv420Program.emplace();
	LinkProgram(
		&*_yuv420Program,
		_texturedVertexShader,
		FragmentShader({
			FragmentSampleYUV420Texture(),
			FragmentApplyControlsFade(),
			FragmentRoundedCorners()
		}));

	_nv12Program.emplace();
	LinkProgram(
		&*_nv12Program,
		_texturedVertexShader,
		FragmentShader({
			FragmentSampleNV12Texture(),
			FragmentApplyControlsFade(),
			FragmentRoundedCorners()
		}));

	_fillProgram.emplace();
	LinkProgram(
		&*_fillProgram,
		VertexShader({ VertexViewportTransform() }),
		FragmentShader({ FragmentStaticColor() }));

	_controlsProgram.emplace();
	LinkProgram(
		&*_controlsProgram,
		_texturedVertexShader,
		FragmentShader({
			FragmentSampleARGB32Texture(),
			FragmentGlobalOpacity(),
		}));

	_roundedCornersProgram.emplace();
	LinkProgram(
		&*_roundedCornersProgram,
		VertexShader({ VertexViewportTransform() }),
		FragmentShader({
			{ .body = "result = vec4(1.);" },
			FragmentRoundedCorners(),
		}));

	const auto renderer = reinterpret_cast<const char*>(
		f.glGetString(GL_RENDERER));
	CrashReports::SetAnnotation(
		"OpenGL Renderer",
		renderer ? renderer : "[nullptr]");
}

void OverlayWidget::RendererGL::deinit(QOpenGLFunctions *f) {
	_textures.destroy(f);
	for (auto i = 0; i != 3; ++i) {
		_rgbaSize[i] = QSize();
		_cacheKeys[i] = 0;
	}
	_lumaSize = QSize();
	_chromaSize = QSize();
	_chromaSizeV = QSize();
	_chromaNV12 = false;
	_trackFrameIndex = 0;
	_streamedIndex = 0;
	_imageProgram = std::nullopt;
	_texturedVertexShader = nullptr;
	_withTransparencyProgram = std::nullopt;
	_yuv420Program = std::nullopt;
	_nv12Program = std::nullopt;
	_fillProgram = std::nullopt;
	_controlsProgram = std::nullopt;
	_contentBuffer = std::nullopt;
	_controlsFadeImage.destroy(f);
	_radialImage.destroy(f);
	_documentBubbleImage.destroy(f);
	_themePreviewImage.destroy(f);
	_saveMsgImage.destroy(f);
	_footerImage.destroy(f);
	_captionImage.destroy(f);
	_groupThumbsImage.destroy(f);
	_controlsImage.destroy(f);
	for (auto &part : _storiesSiblingParts) {
		part.destroy(f);
	}
}

void OverlayWidget::RendererGL::paint(
		not_null<QOpenGLWidget*> widget,
		QOpenGLFunctions &f) {
	if (handleHideWorkaround(f)) {
		return;
	}
	if (const auto stream = _owner->_videoStream.get()) {
		stream->ensureBorrowedRenderer(f);
	}
	const auto factor = widget->devicePixelRatioF();
	if (_factor != factor) {
		_factor = factor;
		_ifactor = int(std::ceil(factor));
		_controlsImage.invalidate();

		// We use the fact that fade texture atlas
		// takes exactly full texture size. In case we
		// just invalidate it we may get larger image
		// in case of moving from greater _factor to lesser.
		_controlsFadeImage.destroy(&f);
	}
	_blendingEnabled = false;
	_viewport = widget->size();
	_uniformViewport = QVector2D(
		_viewport.width() * _factor,
		_viewport.height() * _factor);
	_f = &f;
	_owner->paint(this);
	_f = nullptr;
}

std::optional<QColor> OverlayWidget::RendererGL::clearColor() {
	if (_owner->_hideWorkaround) {
		return QColor(0, 0, 0, 0);
	} else if (_owner->_fullScreenVideo) {
		return st::mediaviewVideoBg->c;
	} else {
		return st::mediaviewBg->c;
	}
}

bool OverlayWidget::RendererGL::handleHideWorkaround(QOpenGLFunctions &f) {
	// This is needed on Windows or Linux,
	// because on reopen it blinks with the last shown content.
	return _owner->_hideWorkaround != nullptr;
}

void OverlayWidget::RendererGL::paintBackground() {
	_contentBuffer->bind();
	if (const auto notch = _owner->topNotchSkip()) {
		const auto top = transformRect(QRect(0, 0, _owner->width(), notch));
		const GLfloat coords[] = {
			top.left(), top.top(),
			top.right(), top.top(),
			top.right(), top.bottom(),
			top.left(), top.bottom(),
		};
		const auto offset = kNotchOffset;
		_contentBuffer->write(
			offset * 4 * sizeof(GLfloat),
			coords,
			sizeof(coords));

		_fillProgram->bind();
		_fillProgram->setUniformValue("viewport", _uniformViewport);
		FillRectangle(
			*_f,
			&*_fillProgram,
			offset,
			QColor(0, 0, 0));
	}
}

void OverlayWidget::RendererGL::paintVideoStream() {
	_owner->_videoStream->borrowedPaint(*_f);
}

void OverlayWidget::RendererGL::paintTransformedVideoFrame(
		ContentGeometry geometry) {
	const auto data = _owner->videoFrameWithInfo();
	if (data.format == Streaming::FrameFormat::None) {
		return;
	} else if (data.format == Streaming::FrameFormat::ARGB32) {
		Assert(!data.image.isNull());
		paintTransformedStaticContent(
			data.image,
			geometry,
			data.alpha,
			data.alpha);
		return;
	} else if (data.format == Streaming::FrameFormat::NativeTexture) {
		const auto image = _owner->currentVideoFrameImage();
		if (!image.isNull()) {
			paintTransformedStaticContent(
				image,
				geometry,
				data.alpha,
				data.alpha);
		}
		return;
	}
	Assert(!data.yuv->size.isEmpty());
	const auto program = (data.format == Streaming::FrameFormat::NV12)
		? &*_nv12Program
		: &*_yuv420Program;
	program->bind();
	const auto nv12 = (data.format == Streaming::FrameFormat::NV12);
	const auto yuv = data.yuv;
	const auto nv12changed = (_chromaNV12 != nv12);

	const auto upload = (_trackFrameIndex != data.index)
		|| (_streamedIndex != _owner->streamedIndex());
	_trackFrameIndex = data.index;
	_streamedIndex = _owner->streamedIndex();

	_f->glActiveTexture(GL_TEXTURE0);
	_textures.bind(*_f, 3);
	if (upload) {
		_f->glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
		uploadTexture(
			GL_ALPHA,
			GL_ALPHA,
			yuv->size,
			_lumaSize,
			yuv->y.stride,
			yuv->y.data);
		_lumaSize = yuv->size;
	}
	_f->glActiveTexture(GL_TEXTURE1);
	_textures.bind(*_f, 4);
	if (upload) {
		uploadTexture(
			nv12 ? GL_RG : GL_ALPHA,
			nv12 ? GL_RG : GL_ALPHA,
			yuv->chromaSize,
			nv12changed ? QSize() : _chromaSize,
			yuv->u.stride / (nv12 ? 2 : 1),
			yuv->u.data);
		_chromaSize = yuv->chromaSize;
		if (nv12) {
			_f->glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
		}
		_chromaNV12 = nv12;
	}

	validateControlsFade();
	if (nv12) {
		_f->glActiveTexture(GL_TEXTURE2);
		_controlsFadeImage.bind(*_f);
	} else {
		_f->glActiveTexture(GL_TEXTURE2);
		_textures.bind(*_f, 5);
		if (upload) {
			uploadTexture(
				GL_ALPHA,
				GL_ALPHA,
				yuv->chromaSize,
				_chromaSizeV,
				yuv->v.stride,
				yuv->v.data);
			_chromaSizeV = yuv->chromaSize;
			_f->glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
		}

		_f->glActiveTexture(GL_TEXTURE3);
		_controlsFadeImage.bind(*_f);
	}
	program->setUniformValue("y_texture", GLint(0));
	if (nv12) {
		program->setUniformValue("uv_texture", GLint(1));
	} else {
		program->setUniformValue("u_texture", GLint(1));
		program->setUniformValue("v_texture", GLint(2));
	}
	program->setUniformValue("f_texture", GLint(nv12 ? 2 : 3));

	toggleBlending(geometry.roundRadius > 0.);
	const auto textureRect = _owner->_stories
		? StoryCropTextureRect(QSizeF(yuv->size), geometry.rect.size())
		: QRectF(0., 0., 1., 1.);
	paintTransformedContent(program, geometry, false, textureRect);

	if (_owner->_recognitionResult.success
		&& !_owner->_recognitionResult.items.empty()) {
		const auto opacity = _owner->_recognitionAnimation.value(
			_owner->_showRecognitionResults ? 1. : 0.);
		if (opacity > 0.) {
			paintRecognitionOverlay(data.image, geometry, opacity);
		}
	}
}

void OverlayWidget::RendererGL::paintTransformedStaticContent(
		const QImage &image,
		ContentGeometry geometry,
		bool semiTransparent,
		bool fillTransparentBackground,
		int index) {
	Expects(index >= 0 && index < 3);
	Expects(image.isNull()
		|| image.format() == QImage::Format_RGB32
		|| image.format() == QImage::Format_ARGB32_Premultiplied);

	if (geometry.rect.isEmpty()) {
		return;
	}

	auto &program = fillTransparentBackground
		? _withTransparencyProgram
		: _staticContentProgram;
	program->bind();
	if (fillTransparentBackground) {
		program->setUniformValue(
			"transparentBg",
			st::mediaviewTransparentBg->c);
		program->setUniformValue(
			"transparentFg",
			st::mediaviewTransparentFg->c);
		program->setUniformValue(
			"transparentSize",
			st::transparentPlaceholderSize * _factor);
	}

	_f->glActiveTexture(GL_TEXTURE0);
	_textures.bind(*_f, index);
	const auto cacheKey = image.isNull() ? qint64(-1) : image.cacheKey();
	const auto upload = (_cacheKeys[index] != cacheKey);
	if (upload) {
		_cacheKeys[index] = cacheKey;
		if (image.isNull()) {
			// Upload transparent 2x2 texture.
			const auto stride = 2;
			const uint32_t data[4] = { 0 };
			uploadTexture(
				Ui::GL::kFormatRGBA,
				Ui::GL::kFormatRGBA,
				QSize(2, 2),
				_rgbaSize[index],
				stride,
				data);
			_rgbaSize[index] = QSize(2, 2);
		} else {
			const auto stride = image.bytesPerLine() / 4;
			const auto data = image.constBits();
			uploadTexture(
				Ui::GL::kFormatRGBA,
				Ui::GL::kFormatRGBA,
				image.size(),
				_rgbaSize[index],
				stride,
				data);
			_rgbaSize[index] = image.size();
		}
	}

	validateControlsFade();
	_f->glActiveTexture(GL_TEXTURE1);
	_controlsFadeImage.bind(*_f);

	program->setUniformValue("s_texture", GLint(0));
	program->setUniformValue("f_texture", GLint(1));

	toggleBlending((geometry.roundRadius > 0.)
		|| (semiTransparent && !fillTransparentBackground));
	const auto textureRect = _owner->_stories
		? StoryCropTextureRect(QSizeF(image.size()), geometry.rect.size())
		: QRectF(0., 0., 1., 1.);
	paintTransformedContent(
		&*program,
		geometry,
		fillTransparentBackground,
		textureRect);

	if (_owner->_recognitionResult.success
		&& !_owner->_recognitionResult.items.empty()
		&& !image.isNull()) {
		const auto opacity = _owner->_recognitionAnimation.value(
			_owner->_showRecognitionResults ? 1. : 0.);
		if (opacity > 0.) {
			paintRecognitionOverlay(image, geometry, opacity);
		}
	}
}

void OverlayWidget::RendererGL::paintTransformedContent(
		not_null<QOpenGLShaderProgram*> program,
		ContentGeometry geometry,
		bool fillTransparentBackground,
		QRectF textureRect) {
	const auto rect = scaleRect(
		transformRect(geometry.rect),
		geometry.scale);
	const auto centerx = rect.x() + rect.width() / 2;
	const auto centery = rect.y() + rect.height() / 2;
	const auto rsin = float(std::sin(geometry.rotation * M_PI / 180.));
	const auto rcos = float(std::cos(geometry.rotation * M_PI / 180.));
	const auto rotated = [&](float x, float y) -> std::array<float, 2> {
		x -= centerx;
		y -= centery;
		return std::array<float, 2>{
			centerx + (x * rcos + y * rsin),
			centery + (y * rcos - x * rsin)
		};
	};
	const auto topleft = rotated(rect.left(), rect.top());
	const auto topright = rotated(rect.right(), rect.top());
	const auto bottomright = rotated(rect.right(), rect.bottom());
	const auto bottomleft = rotated(rect.left(), rect.bottom());
	const auto texLeft = float(textureRect.x());
	const auto texRight = float(textureRect.x() + textureRect.width());
	const auto texTop = 1.f - float(textureRect.y());
	const auto texBottom = 1.f - float(textureRect.y() + textureRect.height());
	const GLfloat coords[] = {
		topleft[0], topleft[1],
		texLeft, texTop,

		topright[0], topright[1],
		texRight, texTop,

		bottomright[0], bottomright[1],
		texRight, texBottom,

		bottomleft[0], bottomleft[1],
		texLeft, texBottom,
	};

	_contentBuffer->bind();
	_contentBuffer->write(0, coords, sizeof(coords));

	program->setUniformValue("viewport", _uniformViewport);
	if (_owner->_stories) {
		const auto &top = st::storiesShadowTop.size();
		const auto shadowTop = geometry.topShadowShown
			? geometry.rect.y()
			: geometry.rect.y() - top.height();
		program->setUniformValue(
			"shadowTopRect",
			Uniform(transformRect(
				QRect(QPoint(geometry.rect.x(), shadowTop), top))));
	} else {
		const auto &top = st::mediaviewShadowTop.size();
		const auto point = QPoint(
			_shadowTopFlip ? 0 : (_viewport.width() - top.width()),
			0);
		program->setUniformValue(
			"shadowTopRect",
			Uniform(transformRect(QRect(point, top))));
	}
	const auto &bottom = _owner->_stories
		? st::storiesShadowBottom
		: st::mediaviewShadowBottom;
	program->setUniformValue("shadowBottomSkipOpacityFullFade", QVector4D(
		bottom.height() * _factor,
		geometry.bottomShadowSkip * _factor,
		geometry.controlsOpacity,
		1.f - float(geometry.fade)));
	if (!fillTransparentBackground) {
		program->setUniformValue(
			"roundRect",
			geometry.roundRadius ? Uniform(rect) : QVector4D(
				0,
				0,
				_uniformViewport.x(),
				_uniformViewport.y()));
		program->setUniformValue(
			"roundRadius",
			GLfloat(geometry.roundRadius * _factor));
	}
	FillTexturedRectangle(*_f, &*program);
}

void OverlayWidget::RendererGL::uploadTexture(
		GLint internalformat,
		GLint format,
		QSize size,
		QSize hasSize,
		int stride,
		const void *data) const {
	_f->glPixelStorei(GL_UNPACK_ROW_LENGTH, stride);
	if (hasSize != size) {
		_f->glTexImage2D(
			GL_TEXTURE_2D,
			0,
			internalformat,
			size.width(),
			size.height(),
			0,
			format,
			GL_UNSIGNED_BYTE,
			data);
	} else {
		_f->glTexSubImage2D(
			GL_TEXTURE_2D,
			0,
			0,
			0,
			size.width(),
			size.height(),
			format,
			GL_UNSIGNED_BYTE,
			data);
	}
	_f->glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
}

void OverlayWidget::RendererGL::paintRadialLoading(
		QRect inner,
		bool radial,
		float64 radialOpacity) {
	paintUsingRaster(_radialImage, inner, [&](Painter &&p) {
		const auto newInner = QRect(QPoint(), inner.size());
		_owner->paintRadialLoadingContent(p, newInner, radial, radialOpacity);
	}, kRadialLoadingOffset, true);
}

void OverlayWidget::RendererGL::paintThemePreview(QRect outer) {
	paintUsingRaster(_themePreviewImage, outer, [&](Painter &&p) {
		p.translate(-outer.topLeft());
		_owner->paintThemePreviewContent(p, outer, outer);
	}, kThemePreviewOffset);
}

void OverlayWidget::RendererGL::paintDocumentBubble(
		QRect outer,
		QRect icon) {
	paintUsingRaster(_documentBubbleImage, outer, [&](Painter &&p) {
		const auto newOuter = QRect(QPoint(), outer.size());
		const auto newIcon = icon.translated(-outer.topLeft());
		_owner->paintDocumentBubbleContent(p, newOuter, newIcon, newOuter);
	}, kDocumentBubbleOffset);
	_owner->paintRadialLoading(this);
}

void OverlayWidget::RendererGL::paintSaveMsg(QRect outer) {
	paintUsingRaster(_saveMsgImage, outer, [&](Painter &&p) {
		const auto newOuter = QRect(QPoint(), outer.size());
		_owner->paintSaveMsgContent(p, newOuter, newOuter);
	}, kSaveMsgOffset, true);
}

void OverlayWidget::RendererGL::paintChapter(QRect outer) {
	paintUsingRaster(_chapterImage, outer, [&](Painter &&p) {
		const auto newOuter = QRect(QPoint(), outer.size());
		_owner->paintChapterContent(p, newOuter, newOuter);
	}, kChapterOffset, true);
}

void OverlayWidget::RendererGL::paintSpeedBoost(QRect outer) {
	paintUsingRaster(_speedBoostImage, outer, [&](Painter &&p) {
		const auto newOuter = QRect(QPoint(), outer.size());
		_owner->paintSpeedBoostContent(p, newOuter, newOuter);
	}, kSpeedBoostOffset, true);
}

void OverlayWidget::RendererGL::paintControlsStart() {
	validateControls();
	_f->glActiveTexture(GL_TEXTURE0);
	_controlsImage.bind(*_f);
	toggleBlending(true);
}

void OverlayWidget::RendererGL::paintControl(
		Over control,
		QRect over,
		float64 overOpacity,
		QRect inner,
		float64 innerOpacity,
		const style::icon &icon) {
	const auto meta = controlMeta(control);
	Assert(meta.icon == &icon);

	const auto overAlpha = overOpacity * kOverBackgroundOpacity;
	const auto offset = kControlsOffset + (meta.index * kControlValues) / 4;
	const auto fgOffset = offset + 4;
	const auto overRect = _controlsImage.texturedRect(
		over,
		_controlsTextures[kControlsCount]);
	const auto overGeometry = transformRect(over);
	const auto iconRect = _controlsImage.texturedRect(
		inner,
		_controlsTextures[meta.index]);
	const auto iconGeometry = transformRect(iconRect.geometry);
	const GLfloat coords[] = {
		overGeometry.left(), overGeometry.top(),
		overRect.texture.left(), overRect.texture.bottom(),

		overGeometry.right(), overGeometry.top(),
		overRect.texture.right(), overRect.texture.bottom(),

		overGeometry.right(), overGeometry.bottom(),
		overRect.texture.right(), overRect.texture.top(),

		overGeometry.left(), overGeometry.bottom(),
		overRect.texture.left(), overRect.texture.top(),

		iconGeometry.left(), iconGeometry.top(),
		iconRect.texture.left(), iconRect.texture.bottom(),

		iconGeometry.right(), iconGeometry.top(),
		iconRect.texture.right(), iconRect.texture.bottom(),

		iconGeometry.right(), iconGeometry.bottom(),
		iconRect.texture.right(), iconRect.texture.top(),

		iconGeometry.left(), iconGeometry.bottom(),
		iconRect.texture.left(), iconRect.texture.top(),
	};
	_controlsProgram->bind();
	_controlsProgram->setUniformValue("viewport", _uniformViewport);
	_contentBuffer->bind();
	if (!over.isEmpty() && overOpacity > 0) {
		_contentBuffer->write(
			offset * 4 * sizeof(GLfloat),
			coords,
			sizeof(coords));
		_controlsProgram->setUniformValue("g_opacity", GLfloat(overAlpha));
		FillTexturedRectangle(*_f, &*_controlsProgram, offset);
	} else {
		_contentBuffer->write(
			fgOffset * 4 * sizeof(GLfloat),
			coords + (fgOffset - offset) * 4,
			sizeof(coords) - (fgOffset - offset) * 4 * sizeof(GLfloat));
	}
	_controlsProgram->setUniformValue("g_opacity", GLfloat(innerOpacity));
	FillTexturedRectangle(*_f, &*_controlsProgram, fgOffset);
}

auto OverlayWidget::RendererGL::controlMeta(Over control) const -> Control {
	const auto stories = [&] {
		return (_owner->_stories != nullptr);
	};
	switch (control) {
	case Over::Left: return {
		0,
		stories() ? &st::storiesLeft : &st::mediaviewLeft
	};
	case Over::Right: return {
		1,
		stories() ? &st::storiesRight : &st::mediaviewRight
	};
	case Over::Save: return {
		2,
		(_owner->saveControlLocked()
			? &st::mediaviewSaveLocked
			: &st::mediaviewSave)
	};
	case Over::Share: return { 3, &st::mediaviewShare };
	case Over::Rotate: return { 4, &st::mediaviewRotate };
	case Over::More: return { 5, &st::mediaviewMore };
	case Over::Draw: return { 6, &st::mediaviewDraw };
	case Over::Recognize: return { 7, &st::mediaviewRecognize };
	}
	Unexpected("Control value in OverlayWidget::RendererGL::ControlIndex.");
}

void OverlayWidget::RendererGL::validateControls() {
	if (!_controlsImage.image().isNull()) {
		return;
	}
	const auto metas = {
		controlMeta(Over::Left),
		controlMeta(Over::Right),
		controlMeta(Over::Save),
		controlMeta(Over::Share),
		controlMeta(Over::Rotate),
		controlMeta(Over::More),
		controlMeta(Over::Draw),
		controlMeta(Over::Recognize),
	};
	auto maxWidth = 0;
	auto fullHeight = 0;
	for (const auto &meta : metas) {
		maxWidth = std::max(meta.icon->width(), maxWidth);
		fullHeight += meta.icon->height();
	}
	maxWidth = std::max(st::mediaviewIconOver, maxWidth);
	fullHeight += st::mediaviewIconOver;
	auto image = QImage(
		QSize(maxWidth, fullHeight) * _ifactor,
		QImage::Format_ARGB32_Premultiplied);
	image.fill(Qt::transparent);
	image.setDevicePixelRatio(_ifactor);
	{
		auto p = QPainter(&image);
		auto index = 0;
		auto height = 0;
		for (const auto &meta : metas) {
			meta.icon->paint(p, 0, height, maxWidth);
			_controlsTextures[index++] = QRect(
				QPoint(0, height) * _ifactor,
				meta.icon->size() * _ifactor);
			height += meta.icon->height();
		}
		auto hq = PainterHighQualityEnabler(p);
		p.setPen(Qt::NoPen);
		p.setBrush(OverBackgroundColor());
		p.drawEllipse(
			QRect(0, height, st::mediaviewIconOver, st::mediaviewIconOver));
		_controlsTextures[index++] = QRect(
			QPoint(0, height) * _ifactor,
			QSize(st::mediaviewIconOver, st::mediaviewIconOver) * _ifactor);
		height += st::mediaviewIconOver;
	}
	_controlsImage.setImage(std::move(image));
}

void OverlayWidget::RendererGL::invalidateControls() {
	_controlsImage.invalidate();
	ranges::fill(_controlsTextures, QRect());
}

void OverlayWidget::RendererGL::validateControlsFade() {
	const auto forStories = (_owner->_stories != nullptr);
	const auto flip = !forStories && !_owner->topShadowOnTheRight();
	if (!_controlsFadeImage.image().isNull()
		&& _shadowTopFlip == flip
		&& _shadowsForStories == forStories) {
		return;
	}
	_shadowTopFlip = flip;
	_shadowsForStories = forStories;
	const auto &top = _shadowsForStories
		? st::storiesShadowTop
		: st::mediaviewShadowTop;
	const auto &bottom = _shadowsForStories
		? st::storiesShadowBottom
		: st::mediaviewShadowBottom;
	const auto width = top.width();
	const auto bottomTop = top.height();
	const auto height = bottomTop + bottom.height();

	auto image = QImage(
		QSize(width, height) * _ifactor,
		QImage::Format_ARGB32_Premultiplied);
	image.fill(Qt::transparent);
	image.setDevicePixelRatio(_ifactor);

	auto p = QPainter(&image);
	top.paint(p, 0, 0, width);
	bottom.fill(
		p,
		QRect(0, bottomTop, width, bottom.height()));
	p.end();

	if (flip) {
		image = std::move(image).mirrored(true, false);
	}

	_controlsFadeImage.setImage(std::move(image));
}

void OverlayWidget::RendererGL::paintFooter(QRect outer, float64 opacity) {
	paintUsingRaster(_footerImage, outer, [&](Painter &&p) {
		const auto newOuter = QRect(QPoint(), outer.size());
		_owner->paintFooterContent(p, newOuter, newOuter, opacity);
	}, kFooterOffset, true);
}

void OverlayWidget::RendererGL::paintCaption(QRect outer, float64 opacity) {
	paintUsingRaster(_captionImage, outer, [&](Painter &&p) {
		const auto newOuter = QRect(QPoint(), outer.size());
		_owner->paintCaptionContent(p, newOuter, newOuter, opacity);
	}, kCaptionOffset, true);
}

void OverlayWidget::RendererGL::paintGroupThumbs(
		QRect outer,
		float64 opacity) {
	paintUsingRaster(_groupThumbsImage, outer, [&](Painter &&p) {
		const auto newOuter = QRect(QPoint(), outer.size());
		_owner->paintGroupThumbsContent(p, newOuter, newOuter, opacity);
	}, kGroupThumbsOffset, true);
}

void OverlayWidget::RendererGL::paintRoundedCorners(int radius) {
	const auto topLeft = transformRect(QRect(0, 0, radius, radius));
	const auto topRight = transformRect(
		QRect(_viewport.width() - radius, 0, radius, radius));
	const auto bottomRight = transformRect(QRect(
		_viewport.width() - radius,
		_viewport.height() - radius,
		radius,
		radius));
	const auto bottomLeft = transformRect(
		QRect(0, _viewport.height() - radius, radius, radius));
	const GLfloat coords[] = {
		topLeft.left(), topLeft.top(),
		topLeft.right(), topLeft.top(),
		topLeft.right(), topLeft.bottom(),
		topLeft.right(), topLeft.bottom(),
		topLeft.left(), topLeft.bottom(),
		topLeft.left(), topLeft.top(),

		topRight.left(), topRight.top(),
		topRight.right(), topRight.top(),
		topRight.right(), topRight.bottom(),
		topRight.right(), topRight.bottom(),
		topRight.left(), topRight.bottom(),
		topRight.left(), topRight.top(),

		bottomRight.left(), bottomRight.top(),
		bottomRight.right(), bottomRight.top(),
		bottomRight.right(), bottomRight.bottom(),
		bottomRight.right(), bottomRight.bottom(),
		bottomRight.left(), bottomRight.bottom(),
		bottomRight.left(), bottomRight.top(),

		bottomLeft.left(), bottomLeft.top(),
		bottomLeft.right(), bottomLeft.top(),
		bottomLeft.right(), bottomLeft.bottom(),
		bottomLeft.right(), bottomLeft.bottom(),
		bottomLeft.left(), bottomLeft.bottom(),
		bottomLeft.left(), bottomLeft.top(),
	};
	const auto offset = kControlsOffset
		+ (kControlsCount * kControlValues) / 4;
	const auto byteOffset = offset * 4 * sizeof(GLfloat);
	_contentBuffer->bind();
	_contentBuffer->write(byteOffset, coords, sizeof(coords));
	_roundedCornersProgram->bind();
	_roundedCornersProgram->setUniformValue("viewport", _uniformViewport);
	const auto roundRect = transformRect(QRect(QPoint(), _viewport));
	_roundedCornersProgram->setUniformValue("roundRect", Uniform(roundRect));
	_roundedCornersProgram->setUniformValue(
		"roundRadius",
		GLfloat(radius * _factor));

	_f->glEnable(GL_BLEND);
	_f->glBlendFunc(GL_ZERO, GL_SRC_ALPHA);

	GLint position = _roundedCornersProgram->attributeLocation("position");
	_f->glVertexAttribPointer(
		position,
		2,
		GL_FLOAT,
		GL_FALSE,
		2 * sizeof(GLfloat),
		reinterpret_cast<const void*>(byteOffset));
	_f->glEnableVertexAttribArray(position);

	_f->glDrawArrays(GL_TRIANGLES, 0, base::array_size(coords) / 2);

	_f->glDisableVertexAttribArray(position);
}

void OverlayWidget::RendererGL::paintStoriesSiblingPart(
		int index,
		const QImage &image,
		QRect rect,
		float64 opacity) {
	Expects(index >= 0 && index < kStoriesSiblingPartsCount);

	if (image.isNull() || rect.isEmpty()) {
		return;
	}

	_f->glActiveTexture(GL_TEXTURE0);

	auto &part = _storiesSiblingParts[index];
	part.setImage(image);
	part.bind(*_f);

	const auto textured = part.texturedRect(
		rect,
		QRect(QPoint(), image.size()));
	const auto geometry = transformRect(textured.geometry);
	const GLfloat coords[] = {
		geometry.left(), geometry.top(),
		textured.texture.left(), textured.texture.bottom(),

		geometry.right(), geometry.top(),
		textured.texture.right(), textured.texture.bottom(),

		geometry.right(), geometry.bottom(),
		textured.texture.right(), textured.texture.top(),

		geometry.left(), geometry.bottom(),
		textured.texture.left(), textured.texture.top(),
	};
	const auto offset = kControlsOffset
		+ (kControlsCount * kControlValues) / 4
		+ (6 * 2 * 4) / 4 // rounding
		+ (index * 4);
	const auto byteOffset = offset * 4 * sizeof(GLfloat);
	_contentBuffer->bind();
	_contentBuffer->write(byteOffset, coords, sizeof(coords));

	_controlsProgram->bind();
	_controlsProgram->setUniformValue("viewport", _uniformViewport);
	_contentBuffer->write(
		offset * 4 * sizeof(GLfloat),
		coords,
		sizeof(coords));
	_controlsProgram->setUniformValue("g_opacity", GLfloat(opacity));
	FillTexturedRectangle(*_f, &*_controlsProgram, offset);
}

//
//void OverlayWidget::RendererGL::invalidate() {
//	_trackFrameIndex = -1;
//	_streamedIndex = -1;
//	const auto images = {
//		&_radialImage,
//		&_documentBubbleImage,
//		&_themePreviewImage,
//		&_saveMsgImage,
//		&_footerImage,
//		&_captionImage,
//		&_groupThumbsImage,
//		&_controlsImage,
//	};
//	for (const auto image : images) {
//		image->setImage(QImage());
//	}
//	invalidateControls();
//}

void OverlayWidget::RendererGL::paintUsingRaster(
		Ui::GL::Image &image,
		QRect rect,
		Fn<void(Painter&&)> method,
		int bufferOffset,
		bool transparent) {
	auto raster = image.takeImage();
	const auto size = rect.size() * _ifactor;
	if (raster.width() < size.width() || raster.height() < size.height()) {
		raster = QImage(size, QImage::Format_ARGB32_Premultiplied);
		Assert(!raster.isNull());
		raster.setDevicePixelRatio(_ifactor);
		if (!transparent
			&& (raster.width() > size.width()
				|| raster.height() > size.height())) {
			raster.fill(Qt::transparent);
		}
	} else if (raster.devicePixelRatio() != _ifactor) {
		raster.setDevicePixelRatio(_ifactor);
	}

	if (transparent) {
		raster.fill(Qt::transparent);
	}
	method(Painter(&raster));

	_f->glActiveTexture(GL_TEXTURE0);

	image.setImage(std::move(raster), size);
	image.bind(*_f);

	const auto textured = image.texturedRect(rect, QRect(QPoint(), size));
	const auto geometry = transformRect(textured.geometry);
	const GLfloat coords[] = {
		geometry.left(), geometry.top(),
		textured.texture.left(), textured.texture.bottom(),

		geometry.right(), geometry.top(),
		textured.texture.right(), textured.texture.bottom(),

		geometry.right(), geometry.bottom(),
		textured.texture.right(), textured.texture.top(),

		geometry.left(), geometry.bottom(),
		textured.texture.left(), textured.texture.top(),
	};
	_contentBuffer->bind();
	_contentBuffer->write(
		bufferOffset * 4 * sizeof(GLfloat),
		coords,
		sizeof(coords));

	_imageProgram->bind();
	_imageProgram->setUniformValue("viewport", _uniformViewport);
	_imageProgram->setUniformValue("s_texture", GLint(0));

	toggleBlending(transparent);
	FillTexturedRectangle(*_f, &*_imageProgram, bufferOffset);
}

void OverlayWidget::RendererGL::toggleBlending(bool enabled) {
	if (_blendingEnabled == enabled) {
		return;
	} else if (enabled) {
		_f->glEnable(GL_BLEND);
		_f->glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
	} else {
		_f->glDisable(GL_BLEND);
	}
	_blendingEnabled = enabled;
}

Rect OverlayWidget::RendererGL::transformRect(const Rect &raster) const {
	return TransformRect(raster, _viewport, _factor);
}

Rect OverlayWidget::RendererGL::transformRect(const QRectF &raster) const {
	return TransformRect(raster, _viewport, _factor);
}

Rect OverlayWidget::RendererGL::transformRect(const QRect &raster) const {
	return TransformRect(Rect(raster), _viewport, _factor);
}

Rect OverlayWidget::RendererGL::scaleRect(
		const Rect &unscaled,
		float64 scale) const {
	const auto added = scale - 1.;
	const auto addw = unscaled.width() * added;
	const auto addh = unscaled.height() * added;
	return Rect(
		unscaled.x() - addw / 2,
		unscaled.y() - addh / 2,
		unscaled.width() + addw,
		unscaled.height() + addh);
}

void OverlayWidget::RendererGL::paintRecognitionOverlay(
		const QImage &image,
		ContentGeometry geometry,
		float64 opacity) {
	const auto rect = scaleRect(
		transformRect(geometry.rect),
		geometry.scale);
	const auto centerx = rect.x() + rect.width() / 2;
	const auto centery = rect.y() + rect.height() / 2;
	const auto radians = float(geometry.rotation * M_PI / 180.);
	const auto rsin = std::sin(radians);
	const auto rcos = std::cos(radians);
	const auto rotated = [&](float x, float y) -> std::array<float, 2> {
		x -= centerx;
		y -= centery;
		return std::array<float, 2>{
			centerx + (x * rcos + y * rsin),
			centery + (y * rcos - x * rsin)
		};
	};

	const auto scale = rect.width() / float64(image.width());
	const auto imageTopLeft = QPointF(rect.left(), rect.top());

	_fillProgram->bind();
	_fillProgram->setUniformValue("viewport", _uniformViewport);
	_contentBuffer->bind();

	_f->glEnable(GL_STENCIL_TEST);
	_f->glClear(GL_STENCIL_BUFFER_BIT);
	_f->glStencilFunc(GL_ALWAYS, 1, 0xFF);
	_f->glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
	_f->glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);

	for (const auto &item : _owner->_recognitionResult.items) {
		const auto &r = item.rect;
		const auto lightRect = QRectF(
			imageTopLeft.x() + r.left() * _ifactor * scale,
			imageTopLeft.y()
				+ (image.height() - (r.y() + r.height()) * _ifactor) * scale,
			r.width() * _ifactor * scale,
			r.height() * _ifactor * scale);
		const auto tl = rotated(lightRect.left(), lightRect.top());
		const auto tr = rotated(lightRect.right(), lightRect.top());
		const auto br = rotated(lightRect.right(), lightRect.bottom());
		const auto bl = rotated(lightRect.left(), lightRect.bottom());
		const GLfloat coords[] = {
			tl[0], tl[1], tr[0], tr[1], br[0], br[1], bl[0], bl[1],
		};
		_contentBuffer->write(0, coords, sizeof(coords));
		FillRectangle(*_f, &*_fillProgram, 0, QColor(255, 255, 255));
	}

	_f->glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
	_f->glStencilFunc(GL_NOTEQUAL, 1, 0xFF);
	_f->glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);

	_f->glEnable(GL_BLEND);
	_f->glBlendFuncSeparate(GL_ZERO, GL_SRC_COLOR, GL_ZERO, GL_ONE);

	const auto tl = rotated(rect.left(), rect.top());
	const auto tr = rotated(rect.right(), rect.top());
	const auto br = rotated(rect.right(), rect.bottom());
	const auto bl = rotated(rect.left(), rect.bottom());
	const auto dark = int(150 + (255 - 150) * (1. - opacity));
	const GLfloat darkRect[] = {
		tl[0], tl[1], tr[0], tr[1], br[0], br[1], bl[0], bl[1],
	};
	_contentBuffer->write(0, darkRect, sizeof(darkRect));
	FillRectangle(*_f, &*_fillProgram, 0, QColor(dark, dark, dark, 255));

	const auto spans = _owner->_recognition.spans();
	if (!spans.empty()) {
		_f->glDisable(GL_STENCIL_TEST);
		_f->glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
		const auto alpha = int(128 * opacity);
		for (const auto &span : spans) {
			const auto band = _owner->_recognition.bandFor(
				span.item,
				span.from,
				span.till);
			if (band.isEmpty()) {
				continue;
			}
			const auto selRect = QRectF(
				imageTopLeft.x() + band.left() * _ifactor * scale,
				imageTopLeft.y()
					+ (image.height() - (band.y() + band.height()) * _ifactor)
						* scale,
				band.width() * _ifactor * scale,
				band.height() * _ifactor * scale);
			const auto stl = rotated(selRect.left(), selRect.top());
			const auto str = rotated(selRect.right(), selRect.top());
			const auto sbr = rotated(selRect.right(), selRect.bottom());
			const auto sbl = rotated(selRect.left(), selRect.bottom());
			const GLfloat selCoords[] = {
				stl[0], stl[1], str[0], str[1],
				sbr[0], sbr[1], sbl[0], sbl[1],
			};
			_contentBuffer->write(0, selCoords, sizeof(selCoords));
			FillRectangle(*_f, &*_fillProgram, 0, QColor(48, 128, 255, alpha));
		}
	}

	_f->glDisable(GL_BLEND);
	_f->glDisable(GL_STENCIL_TEST);
}

} // namespace Media::View
