/*
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 "data/data_document_media.h"

#include "data/data_document.h"
#include "data/data_document_resolver.h"
#include "data/data_session.h"
#include "data/data_file_origin.h"
#include "media/clip/media_clip_reader.h"
#include "main/main_session.h"
#include "main/main_session_settings.h"
#include "lottie/lottie_animation.h"
#include "lottie/lottie_frame_generator.h"
#include "ffmpeg/ffmpeg_frame_generator.h"
#include "history/history_item.h"
#include "history/history.h"
#include "window/themes/window_theme_preview.h"
#include "core/core_settings.h"
#include "core/application.h"
#include "core/mime_type.h"
#include "storage/file_download.h"
#include "ui/chat/attach/attach_prepare.h"

#include <QtCore/QBuffer>
#include <QtGui/QImageReader>

namespace Data {
namespace {

constexpr auto kReadAreaLimit = 12'032 * 9'024;
constexpr auto kWallPaperThumbnailLimit = 960;
constexpr auto kGoodThumbQuality = 87;

enum class FileType {
	Video,
	VideoSticker,
	AnimatedSticker,
	WallPaper,
	WallPatternPNG,
	WallPatternSVG,
	Theme,
};

[[nodiscard]] bool MayHaveGoodThumbnail(not_null<DocumentData*> owner) {
	return owner->isVideoFile()
		|| owner->isAnimation()
		|| owner->isWallPaper()
		|| owner->isTheme()
		|| (owner->sticker() && owner->sticker()->isAnimated());
}

[[nodiscard]] QImage PrepareGoodThumbnail(
		const QString &path,
		QByteArray data,
		FileType type) {
	if (type == FileType::Video || type == FileType::VideoSticker) {
		auto result = v::get<Ui::PreparedFileInformation::Video>(
			::Media::Clip::PrepareForSending(path, data).media);
		if (result.isWebmSticker && type == FileType::Video) {
			result.thumbnail = Images::Opaque(std::move(result.thumbnail));
		}
		return result.thumbnail;
	} else if (type == FileType::AnimatedSticker) {
		return Lottie::ReadThumbnail(Lottie::ReadContent(data, path));
	} else if (type == FileType::Theme) {
		return Window::Theme::GeneratePreview(data, path);
	} else if (type == FileType::WallPatternSVG) {
		return Images::Read({
			.path = path,
			.content = std::move(data),
			.maxSize = QSize(
				kWallPaperThumbnailLimit,
				kWallPaperThumbnailLimit),
			.gzipSvg = true,
		}).image;
	}
	auto buffer = QBuffer(&data);
	auto file = QFile(path);
	auto device = data.isEmpty() ? static_cast<QIODevice*>(&file) : &buffer;
	auto reader = QImageReader(device);
	const auto size = reader.size();
	if (!reader.canRead()
		|| (size.width() * size.height() > kReadAreaLimit)) {
		return QImage();
	}
	auto result = reader.read();
	if (!result.width() || !result.height()) {
		return QImage();
	}
	return (result.width() > kWallPaperThumbnailLimit
		|| result.height() > kWallPaperThumbnailLimit)
		? result.scaled(
			kWallPaperThumbnailLimit,
			kWallPaperThumbnailLimit,
			Qt::KeepAspectRatio,
			Qt::SmoothTransformation)
		: result;
}

} // namespace

VideoPreviewState::VideoPreviewState(DocumentMedia *media)
: _media(media)
, _usingThumbnail(media ? media->owner()->hasVideoThumbnail() : false) {
}

void VideoPreviewState::automaticLoad(Data::FileOrigin origin) const {
	Expects(_media != nullptr);

	if (_usingThumbnail) {
		_media->videoThumbnailWanted(origin);
	} else {
		_media->automaticLoad(origin, nullptr);
	}
}

::Media::Clip::ReaderPointer VideoPreviewState::makeAnimation(
		Fn<void(::Media::Clip::Notification)> callback) const {
	Expects(_media != nullptr);
	Expects(loaded());

	return _usingThumbnail
		? ::Media::Clip::MakeReader(
			_media->videoThumbnailContent(),
			std::move(callback))
		: ::Media::Clip::MakeReader(
			_media->owner()->location(),
			_media->bytes(),
			std::move(callback));
}

bool VideoPreviewState::usingThumbnail() const {
	return _usingThumbnail;
}

bool VideoPreviewState::loading() const {
	return _usingThumbnail
		? _media->owner()->videoThumbnailLoading()
		: _media
		? _media->owner()->loading()
		: false;
}

bool VideoPreviewState::loaded() const {
	return _usingThumbnail
		? !_media->videoThumbnailContent().isEmpty()
		: _media
		? _media->loaded()
		: false;
}

DocumentMedia::DocumentMedia(not_null<DocumentData*> owner)
: _owner(owner) {
}

// NB! Right now DocumentMedia can outlive Main::Session!
// In DocumentData::collectLocalData a shared_ptr is sent on_main.
// In case this is a problem the ~Gif code should be rewritten.
DocumentMedia::~DocumentMedia() = default;

not_null<DocumentData*> DocumentMedia::owner() const {
	return _owner;
}

void DocumentMedia::goodThumbnailWanted() {
	_flags |= Flag::GoodThumbnailWanted;
}

Image *DocumentMedia::goodThumbnail() const {
	Expects((_flags & Flag::GoodThumbnailWanted) != 0);

	if (!_goodThumbnail) {
		ReadOrGenerateThumbnail(_owner);
	}
	return _goodThumbnail.get();
}

void DocumentMedia::setGoodThumbnail(QImage thumbnail) {
	if (!(_flags & Flag::GoodThumbnailWanted)) {
		return;
	}
	_goodThumbnail = std::make_unique<Image>(std::move(thumbnail));
	_owner->session().notifyDownloaderTaskFinished();
}

Image *DocumentMedia::thumbnailInline() const {
	if (!_inlineThumbnail && !_owner->inlineThumbnailIsPath()) {
		const auto bytes = _owner->inlineThumbnailBytes();
		if (!bytes.isEmpty()) {
			auto image = Images::FromInlineBytes(bytes);
			if (image.isNull()) {
				_owner->clearInlineThumbnailBytes();
			} else {
				_inlineThumbnail = std::make_unique<Image>(std::move(image));
			}
		}
	}
	return _inlineThumbnail.get();
}

const QPainterPath &DocumentMedia::thumbnailPath() const {
	if (_pathThumbnail.isEmpty() && _owner->inlineThumbnailIsPath()) {
		const auto bytes = _owner->inlineThumbnailBytes();
		if (!bytes.isEmpty()) {
			_pathThumbnail = Images::PathFromInlineBytes(bytes);
			if (_pathThumbnail.isEmpty()) {
				_owner->clearInlineThumbnailBytes();
			}
		}
	}
	return _pathThumbnail;
}

Image *DocumentMedia::thumbnail() const {
	return _thumbnail.get();
}

void DocumentMedia::thumbnailWanted(Data::FileOrigin origin) {
	if (!_thumbnail) {
		_owner->loadThumbnail(origin);
	}
}

QSize DocumentMedia::thumbnailSize() const {
	if (const auto image = _thumbnail.get()) {
		return image->size();
	}
	const auto &location = _owner->thumbnailLocation();
	return { location.width(), location.height() };
}

void DocumentMedia::setThumbnail(QImage thumbnail) {
	_thumbnail = std::make_unique<Image>(std::move(thumbnail));
	_owner->session().notifyDownloaderTaskFinished();
}

QByteArray DocumentMedia::videoThumbnailContent() const {
	return _videoThumbnailBytes;
}

QSize DocumentMedia::videoThumbnailSize() const {
	const auto &location = _owner->videoThumbnailLocation();
	return { location.width(), location.height() };
}

void DocumentMedia::videoThumbnailWanted(Data::FileOrigin origin) {
	if (_videoThumbnailBytes.isEmpty()) {
		_owner->loadVideoThumbnail(origin);
	}
}

void DocumentMedia::setVideoThumbnail(QByteArray content) {
	_videoThumbnailBytes = std::move(content);
	_videoThumbnailBytes.detach();
}

void DocumentMedia::checkStickerLarge() {
	if (_sticker) {
		return;
	}
	const auto data = _owner->sticker();
	if (!data) {
		return;
	}
	automaticLoad(_owner->stickerSetOrigin(), nullptr);
	if (data->isAnimated() || !loaded()) {
		return;
	}
	if (_bytes.isEmpty()) {
		const auto &loc = _owner->location(true);
		if (loc.accessEnable()) {
			_sticker = std::make_unique<Image>(loc.name());
			loc.accessDisable();
		}
	} else {
		_sticker = std::make_unique<Image>(_bytes);
	}
}

void DocumentMedia::automaticLoad(
		Data::FileOrigin origin,
		const HistoryItem *item) {
	if (_owner->status != FileReady
		|| loaded()
		|| _owner->uploading()
		|| _owner->cancelled()) {
		return;
	} else if (!item && !_owner->sticker() && !_owner->isAnimation()) {
		return;
	}
	const auto toCache = _owner->saveToCache();
	if (!toCache && !Core::App().canSaveFileWithoutAskingForPath()) {
		// We need a filename, but we're supposed to ask user for it.
		// No automatic download in this case.
		return;
	}
	const auto indata = _owner->filename();
	const auto filename = toCache
		? QString()
		: DocumentFileNameForSave(_owner);
	const auto shouldLoadFromCloud = (indata.isEmpty()
		|| Core::DetectNameType(indata) != Core::NameType::Executable)
		&& (item
			? Data::AutoDownload::Should(
				_owner->session().settings().autoDownload(),
				item->history()->peer,
				_owner)
			: Data::AutoDownload::Should(
				_owner->session().settings().autoDownload(),
				_owner));
	const auto loadFromCloud = shouldLoadFromCloud
		? LoadFromCloudOrLocal
		: LoadFromLocalOnly;
	_owner->save(
		origin,
		filename,
		loadFromCloud,
		true);
}

void DocumentMedia::collectLocalData(not_null<DocumentMedia*> local) {
	if (const auto image = local->_goodThumbnail.get()) {
		_goodThumbnail = std::make_unique<Image>(image->original());
	}
	if (const auto image = local->_inlineThumbnail.get()) {
		_inlineThumbnail = std::make_unique<Image>(image->original());
	}
	if (const auto image = local->_thumbnail.get()) {
		_thumbnail = std::make_unique<Image>(image->original());
	}
	if (const auto image = local->_sticker.get()) {
		_sticker = std::make_unique<Image>(image->original());
	}
	_bytes = local->_bytes;
	_videoThumbnailBytes = local->_videoThumbnailBytes;
	_flags = local->_flags;
}

void DocumentMedia::setBytes(const QByteArray &bytes) {
	if (!bytes.isEmpty()) {
		_bytes = bytes;
	}
}

QByteArray DocumentMedia::bytes() const {
	return _bytes;
}

bool DocumentMedia::loaded(bool check) const {
	return !_bytes.isEmpty() || !_owner->filepath(check).isEmpty();
}

float64 DocumentMedia::progress() const {
	return (_owner->uploading() || _owner->loading())
		? _owner->progress()
		: (loaded() ? 1. : 0.);
}

bool DocumentMedia::canBePlayed() const {
	return !_owner->inappPlaybackFailed()
		&& _owner->useStreamingLoader()
		&& (loaded() || _owner->canBeStreamed());
}

bool DocumentMedia::thumbnailEnoughForSticker() const {
	const auto &location = owner()->thumbnailLocation();
	const auto size = _thumbnail
		? QSize(_thumbnail->width(), _thumbnail->height())
		: location.valid()
		? QSize(location.width(), location.height())
		: QSize();
	return (size.width() >= 128) || (size.height() >= 128);
}

void DocumentMedia::checkStickerSmall() {
	const auto data = _owner->sticker();
	if ((data && data->isAnimated()) || thumbnailEnoughForSticker()) {
		_owner->loadThumbnail(_owner->stickerSetOrigin());
		if (data && data->isAnimated()) {
			automaticLoad(_owner->stickerSetOrigin(), nullptr);
		}
	} else {
		checkStickerLarge();
	}
}

Image *DocumentMedia::getStickerLarge() {
	checkStickerLarge();
	return _sticker.get();
}

Image *DocumentMedia::getStickerSmall() {
	const auto data = _owner->sticker();
	if ((data && data->isAnimated()) || thumbnailEnoughForSticker()) {
		return thumbnail();
	}
	return _sticker.get();
}

void DocumentMedia::checkStickerLarge(not_null<FileLoader*> loader) {
	if (_sticker || !_owner->sticker()) {
		return;
	}
	if (auto image = loader->imageData(); !image.isNull()) {
		_sticker = std::make_unique<Image>(std::move(image));
	}
}

void DocumentMedia::GenerateGoodThumbnail(
		not_null<DocumentData*> document,
		QByteArray data) {
	const auto type = document->isPatternWallPaperSVG()
		? FileType::WallPatternSVG
		: document->isPatternWallPaperPNG()
		? FileType::WallPatternPNG
		: document->isWallPaper()
		? FileType::WallPaper
		: document->isTheme()
		? FileType::Theme
		: !document->sticker()
		? FileType::Video
		: document->sticker()->isLottie()
		? FileType::AnimatedSticker
		: FileType::VideoSticker;
	auto location = document->location().isEmpty()
		? nullptr
		: std::make_unique<Core::FileLocation>(document->location());
	if (data.isEmpty() && !location) {
		document->setGoodThumbnailChecked(false);
		return;
	}
	const auto guard = base::make_weak(&document->session());
	crl::async([=, location = std::move(location)] {
		const auto filepath = (location && location->accessEnable())
			? location->name()
			: QString();
		auto result = PrepareGoodThumbnail(filepath, data, type);
		auto bytes = QByteArray();
		if (!result.isNull()) {
			auto buffer = QBuffer(&bytes);
			const auto format = (type == FileType::AnimatedSticker
				|| type == FileType::VideoSticker)
				? "WEBP"
				: (type == FileType::WallPatternPNG
					|| type == FileType::WallPatternSVG)
				? "PNG"
				: "JPG";
			result.save(&buffer, format, kGoodThumbQuality);
		}
		if (!filepath.isEmpty()) {
			location->accessDisable();
		}
		const auto cache = bytes.isEmpty() ? QByteArray("(failed)") : bytes;
		crl::on_main(guard, [=] {
			document->setGoodThumbnailChecked(true);
			if (const auto active = document->activeMediaView()) {
				active->setGoodThumbnail(result);
			}
			document->owner().cache().put(
				document->goodThumbnailCacheKey(),
				Storage::Cache::Database::TaggedValue{
					base::duplicate(cache),
					kImageCacheTag });
		});
	});
}

void DocumentMedia::CheckGoodThumbnail(not_null<DocumentData*> document) {
	if (!document->goodThumbnailChecked()) {
		ReadOrGenerateThumbnail(document);
	}
}

void DocumentMedia::ReadOrGenerateThumbnail(
		not_null<DocumentData*> document) {
	if (document->goodThumbnailGenerating()
		|| document->goodThumbnailNoData()
		|| !MayHaveGoodThumbnail(document)) {
		return;
	}
	document->setGoodThumbnailGenerating();

	const auto guard = base::make_weak(&document->session());
	const auto active = document->activeMediaView();
	const auto got = [=](QByteArray value) {
		if (value.isEmpty()) {
			const auto bytes = active ? active->bytes() : QByteArray();
			crl::on_main(guard, [=] {
				GenerateGoodThumbnail(document, bytes);
			});
		} else if (active) {
			crl::async([=] {
				auto image = Images::Read({ .content = value }).image;
				crl::on_main(guard, [=, image = std::move(image)]() mutable {
					document->setGoodThumbnailChecked(true);
					if (const auto active = document->activeMediaView()) {
						active->setGoodThumbnail(std::move(image));
					}
				});
			});
		} else {
			crl::on_main(guard, [=] {
				document->setGoodThumbnailChecked(true);
			});
		}
	};
	document->owner().cache().get(document->goodThumbnailCacheKey(), got);
}

auto DocumentIconFrameGenerator(not_null<DocumentMedia*> media)
-> FnMut<std::unique_ptr<Ui::FrameGenerator>()> {
	if (!media->loaded()) {
		return nullptr;
	}
	using Type = StickerType;
	const auto document = media->owner();
	const auto content = media->bytes();
	const auto fromFile = content.isEmpty();
	const auto type = document->sticker()
		? document->sticker()->type
		: (document->isVideoFile() || document->isAnimation())
		? Type::Webm
		: Type::Webp;
	const auto &location = media->owner()->location(true);
	if (fromFile && !location.accessEnable()) {
		return nullptr;
	}
	return [=]() -> std::unique_ptr<Ui::FrameGenerator> {
		const auto bytes = Lottie::ReadContent(content, location.name());
		if (fromFile) {
			location.accessDisable();
		}
		if (bytes.isEmpty()) {
			return nullptr;
		}
		switch (type) {
		case Type::Tgs:
			return std::make_unique<Lottie::FrameGenerator>(bytes);
		case Type::Webm:
			return std::make_unique<FFmpeg::FrameGenerator>(bytes);
		case Type::Webp:
			return std::make_unique<Ui::ImageFrameGenerator>(bytes);
		}
		Unexpected("Document type in DocumentIconFrameGenerator.");
	};
}

auto DocumentIconFrameGenerator(const std::shared_ptr<DocumentMedia> &media)
-> FnMut<std::unique_ptr<Ui::FrameGenerator>()> {
	return DocumentIconFrameGenerator(media.get());
}

} // namespace Data
