/*
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 "storage/localstorage.h"

#include "storage/serialize_common.h"
#include "storage/storage_account.h"
#include "storage/details/storage_file_utilities.h"
#include "storage/details/storage_settings_scheme.h"
#include "data/data_session.h"
#include "data/data_document.h"
#include "data/data_document_media.h"
#include "base/platform/base_platform_info.h"
#include "base/random.h"
#include "ui/power_saving.h"
#include "core/update_checker.h"
#include "core/file_location.h"
#include "core/application.h"
#include "core/core_settings.h"
#include "media/audio/media_audio.h"
#include "mtproto/mtproto_config.h"
#include "mtproto/mtproto_dc_options.h"
#include "main/main_domain.h"
#include "main/main_account.h"
#include "main/main_session.h"
#include "window/themes/window_theme.h"
#include "lang/lang_instance.h"

#include <QtCore/QDirIterator>

#ifndef Q_OS_WIN
#include <unistd.h>
#endif // Q_OS_WIN

//extern "C" {
//#include <openssl/evp.h>
//} // extern "C"

namespace Local {
namespace {

constexpr auto kThemeFileSizeLimit = 5 * 1024 * 1024;
constexpr auto kFileLoaderQueueStopTimeout = crl::time(5000);

constexpr auto kSavedBackgroundFormat = QImage::Format_ARGB32_Premultiplied;
constexpr auto kWallPaperLegacySerializeTagId = int32(-111);
constexpr auto kWallPaperSerializeTagId = int32(-112);
constexpr auto kWallPaperSidesLimit = 10'000;

const auto kThemeNewPathRelativeTag = u"special://new_tag"_q;

using namespace Storage::details;
using Storage::FileKey;

using Database = Storage::Cache::Database;

QString _basePath, _userBasePath, _userDbPath;

TaskQueue *_localLoader = nullptr;

QByteArray _settingsSalt;

auto OldKey = MTP::AuthKeyPtr();
auto SettingsKey = MTP::AuthKeyPtr();

FileKey _themeKeyDay = 0;
FileKey _themeKeyNight = 0;

// Theme key legacy may be read in start() with settings.
// But it should be moved to keyDay or keyNight inside InitialLoadTheme()
// and never used after.
FileKey _themeKeyLegacy = 0;
FileKey _langPackKey = 0;
FileKey _languagesKey = 0;

FileKey _backgroundKeyDay = 0;
FileKey _backgroundKeyNight = 0;
bool _useGlobalBackgroundKeys = false;
bool _backgroundCanWrite = true;

int32 _oldSettingsVersion = 0;
bool _settingsRewriteNeeded = false;
bool _settingsWriteAllowed = false;

enum class WriteMapWhen {
	Now,
	Fast,
	Soon,
};

bool CheckStreamStatus(QDataStream &stream) {
	if (stream.status() != QDataStream::Ok) {
		LOG(("Bad data stream status: %1").arg(stream.status()));
		return false;
	}
	return true;
}

[[nodiscard]] const MTP::Config &LookupFallbackConfig() {
	static const auto lookupConfig = [](not_null<Main::Account*> account) {
		const auto mtp = &account->mtp();
		const auto production = MTP::Environment::Production;
		return (mtp->environment() == production)
			? &mtp->config()
			: nullptr;
	};
	const auto &app = Core::App();
	const auto &domain = app.domain();
	if (!domain.started()) {
		return app.fallbackProductionConfig();
	}
	if (const auto result = lookupConfig(&app.activeAccount())) {
		return *result;
	}
	for (const auto &[_, account] : domain.accounts()) {
		if (const auto result = lookupConfig(account.get())) {
			return *result;
		}
	}
	return app.fallbackProductionConfig();
}

void applyReadContext(ReadSettingsContext &&context) {
	ApplyReadFallbackConfig(context);

	DEBUG_LOG(("Theme: applying context, legacy: %1, day: %2, night: %3"
		).arg(context.themeKeyLegacy
		).arg(context.themeKeyDay
		).arg(context.themeKeyNight));
	_themeKeyLegacy = context.themeKeyLegacy;
	_themeKeyDay = context.themeKeyDay;
	_themeKeyNight = context.themeKeyNight;
	_backgroundKeyDay = context.backgroundKeyDay;
	_backgroundKeyNight = context.backgroundKeyNight;
	_useGlobalBackgroundKeys = context.backgroundKeysRead;
	_langPackKey = context.langPackKey;
	_languagesKey = context.languagesKey;
}

bool _readOldSettings(bool remove, ReadSettingsContext &context) {
	bool result = false;
	auto file = QFile(cWorkingDir() + u"tdata/config"_q);
	if (file.open(QIODevice::ReadOnly)) {
		LOG(("App Info: reading old config..."));
		QDataStream stream(&file);
		stream.setVersion(QDataStream::Qt_5_1);

		qint32 version = 0;
		while (!stream.atEnd()) {
			quint32 blockId;
			stream >> blockId;
			if (!CheckStreamStatus(stream)) break;

			if (blockId == dbiVersion) {
				stream >> version;
				if (!CheckStreamStatus(stream)) break;

				if (version > AppVersion) break;
			} else if (!ReadSetting(blockId, stream, version, context)) {
				break;
			}
		}
		file.close();
		result = true;
	}
	if (remove) file.remove();
	return result;
}

void _readOldUserSettingsFields(
		QIODevice *device,
		qint32 &version,
		ReadSettingsContext &context) {
	QDataStream stream(device);
	stream.setVersion(QDataStream::Qt_5_1);

	while (!stream.atEnd()) {
		quint32 blockId;
		stream >> blockId;
		if (!CheckStreamStatus(stream)) {
			break;
		}

		if (blockId == dbiVersion) {
			stream >> version;
			if (!CheckStreamStatus(stream)) {
				break;
			}

			if (version > AppVersion) return;
		} else if (blockId == dbiEncryptedWithSalt) {
			QByteArray salt, data, decrypted;
			stream >> salt >> data;
			if (!CheckStreamStatus(stream)) {
				break;
			}

			if (salt.size() != 32) {
				LOG(("App Error: bad salt in old user config encrypted part, size: %1").arg(salt.size()));
				continue;
			}

			OldKey = CreateLegacyLocalKey(QByteArray(), salt);

			if (data.size() <= 16 || (data.size() & 0x0F)) {
				LOG(("App Error: bad encrypted part size in old user config: %1").arg(data.size()));
				continue;
			}
			uint32 fullDataLen = data.size() - 16;
			decrypted.resize(fullDataLen);
			const char *dataKey = data.constData(), *encrypted = data.constData() + 16;
			aesDecryptLocal(encrypted, decrypted.data(), fullDataLen, OldKey, dataKey);
			uchar sha1Buffer[20];
			if (memcmp(hashSha1(decrypted.constData(), decrypted.size(), sha1Buffer), dataKey, 16)) {
				LOG(("App Error: bad decrypt key, data from old user config not decrypted"));
				continue;
			}
			uint32 dataLen = *(const uint32*)decrypted.constData();
			if (dataLen > uint32(decrypted.size()) || dataLen <= fullDataLen - 16 || dataLen < 4) {
				LOG(("App Error: bad decrypted part size in old user config: %1, fullDataLen: %2, decrypted size: %3").arg(dataLen).arg(fullDataLen).arg(decrypted.size()));
				continue;
			}
			decrypted.resize(dataLen);
			QBuffer decryptedStream(&decrypted);
			decryptedStream.open(QIODevice::ReadOnly);
			decryptedStream.seek(4); // skip size
			LOG(("App Info: reading encrypted old user config..."));

			_readOldUserSettingsFields(&decryptedStream, version, context);
		} else if (!ReadSetting(blockId, stream, version, context)) {
			return;
		}
	}
}

bool _readOldUserSettings(bool remove, ReadSettingsContext &context) {
	bool result = false;
	// We dropped old test authorizations when migrated to multi auth.
	//const auto testPrefix = (cTestMode() ? u"_test"_q : QString());
	const auto testPrefix = QString();
	QFile file(cWorkingDir() + cDataFile() + testPrefix + u"_config"_q);
	if (file.open(QIODevice::ReadOnly)) {
		LOG(("App Info: reading old user config..."));
		qint32 version = 0;
		_readOldUserSettingsFields(&file, version, context);
		file.close();
		result = true;
	}
	if (remove) file.remove();
	return result;
}

void _readOldMtpDataFields(
		QIODevice *device,
		qint32 &version,
		ReadSettingsContext &context) {
	QDataStream stream(device);
	stream.setVersion(QDataStream::Qt_5_1);

	while (!stream.atEnd()) {
		quint32 blockId;
		stream >> blockId;
		if (!CheckStreamStatus(stream)) {
			break;
		}

		if (blockId == dbiVersion) {
			stream >> version;
			if (!CheckStreamStatus(stream)) {
				break;
			}

			if (version > AppVersion) return;
		} else if (blockId == dbiEncrypted) {
			QByteArray data, decrypted;
			stream >> data;
			if (!CheckStreamStatus(stream)) {
				break;
			}

			if (!OldKey) {
				LOG(("MTP Error: reading old encrypted keys without old key!"));
				continue;
			}

			if (data.size() <= 16 || (data.size() & 0x0F)) {
				LOG(("MTP Error: bad encrypted part size in old keys: %1").arg(data.size()));
				continue;
			}
			uint32 fullDataLen = data.size() - 16;
			decrypted.resize(fullDataLen);
			const char *dataKey = data.constData(), *encrypted = data.constData() + 16;
			aesDecryptLocal(encrypted, decrypted.data(), fullDataLen, OldKey, dataKey);
			uchar sha1Buffer[20];
			if (memcmp(hashSha1(decrypted.constData(), decrypted.size(), sha1Buffer), dataKey, 16)) {
				LOG(("MTP Error: bad decrypt key, data from old keys not decrypted"));
				continue;
			}
			uint32 dataLen = *(const uint32*)decrypted.constData();
			if (dataLen > uint32(decrypted.size()) || dataLen <= fullDataLen - 16 || dataLen < 4) {
				LOG(("MTP Error: bad decrypted part size in old keys: %1, fullDataLen: %2, decrypted size: %3").arg(dataLen).arg(fullDataLen).arg(decrypted.size()));
				continue;
			}
			decrypted.resize(dataLen);
			QBuffer decryptedStream(&decrypted);
			decryptedStream.open(QIODevice::ReadOnly);
			decryptedStream.seek(4); // skip size
			LOG(("App Info: reading encrypted old keys..."));

			_readOldMtpDataFields(&decryptedStream, version, context);
		} else if (!ReadSetting(blockId, stream, version, context)) {
			return;
		}
	}
}

bool _readOldMtpData(bool remove, ReadSettingsContext &context) {
	bool result = false;
	// We dropped old test authorizations when migrated to multi auth.
	//const auto testPostfix = (cTestMode() ? u"_test"_q : QString());
	const auto testPostfix = QString();
	QFile file(cWorkingDir() + cDataFile() + testPostfix);
	if (file.open(QIODevice::ReadOnly)) {
		LOG(("App Info: reading old keys..."));
		qint32 version = 0;
		_readOldMtpDataFields(&file, version, context);
		file.close();
		result = true;
	}
	if (remove) file.remove();
	return result;
}

} // namespace

void sync() {
	Storage::details::Sync();
}

void finish() {
	delete base::take(_localLoader);
	Storage::details::Finish();
}

void InitialLoadTheme();
bool ApplyDefaultNightMode();
void readLangPack();

void start() {
	Expects(_basePath.isEmpty());

	_localLoader = new TaskQueue(kFileLoaderQueueStopTimeout);

	_basePath = cWorkingDir() + u"tdata/"_q;
	if (!QDir().exists(_basePath)) QDir().mkpath(_basePath);

	ReadSettingsContext context;
	FileReadDescriptor settingsData;
	// We dropped old test authorizations when migrated to multi auth.
	//const auto name = cTestMode() ? u"settings_test"_q : u"settings"_q;
	const auto name = u"settings"_q;
	if (!ReadFile(settingsData, name, _basePath)) {
		_readOldSettings(true, context);
		_readOldUserSettings(false, context); // needed further in _readUserSettings
		_readOldMtpData(false, context); // needed further in _readMtpData
		applyReadContext(std::move(context));

		_settingsRewriteNeeded = true;
		ApplyDefaultNightMode();
		return;
	}
	LOG(("App Info: reading settings..."));

	QByteArray salt, settingsEncrypted;
	settingsData.stream >> salt >> settingsEncrypted;
	if (!CheckStreamStatus(settingsData.stream)) {
		return writeSettings();
	}

	if (salt.size() != LocalEncryptSaltSize) {
		LOG(("App Error: bad salt in settings file, size: %1").arg(salt.size()));
		return writeSettings();
	}
	SettingsKey = CreateLegacyLocalKey(QByteArray(), salt);

	EncryptedDescriptor settings;
	if (!DecryptLocal(settings, settingsEncrypted, SettingsKey)) {
		LOG(("App Error: could not decrypt settings from settings file..."));
		return writeSettings();
	}

	LOG(("App Info: reading encrypted settings..."));
	while (!settings.stream.atEnd()) {
		quint32 blockId;
		settings.stream >> blockId;
		if (!CheckStreamStatus(settings.stream)) {
			return writeSettings();
		}

		if (!ReadSetting(blockId, settings.stream, settingsData.version, context)) {
			return writeSettings();
		}
	}

	_oldSettingsVersion = settingsData.version;
	_settingsSalt = salt;

	applyReadContext(std::move(context));
	if (context.legacyRead) {
		writeSettings();
	}

	InitialLoadTheme();

	if (context.tileRead && _useGlobalBackgroundKeys) {
		Window::Theme::Background()->setTileDayValue(context.tileDay);
		Window::Theme::Background()->setTileNightValue(context.tileNight);
	}

	readLangPack();
}

void writeSettings() {
	if (!_settingsWriteAllowed) {
		_settingsRewriteNeeded = true;

		// We need to generate SettingsKey anyway,
		// for the moveLegacyBackground to work.
		if (SettingsKey) {
			return;
		}
	}
	if (_basePath.isEmpty()) {
		LOG(("App Error: _basePath is empty in writeSettings()"));
		return;
	}

	if (!QDir().exists(_basePath)) QDir().mkpath(_basePath);

	// We dropped old test authorizations when migrated to multi auth.
	//const auto name = cTestMode() ? u"settings_test"_q : u"settings"_q;
	const auto name = u"settings"_q;
	FileWriteDescriptor settings(name, _basePath);
	if (_settingsSalt.isEmpty() || !SettingsKey) {
		_settingsSalt.resize(LocalEncryptSaltSize);
		base::RandomFill(_settingsSalt.data(), _settingsSalt.size());
		SettingsKey = CreateLegacyLocalKey(QByteArray(), _settingsSalt);
	}
	settings.writeData(_settingsSalt);

	if (!_settingsWriteAllowed) {
		EncryptedDescriptor data(0);
		settings.writeEncrypted(data, SettingsKey);
		return;
	}
	const auto configSerialized = LookupFallbackConfig().serialize();
	const auto applicationSettings = Core::App().settings().serialize();

	quint32 size = 9 * (sizeof(quint32) + sizeof(qint32));
	size += sizeof(quint32) + Serialize::bytearraySize(configSerialized);
	size += sizeof(quint32) + Serialize::bytearraySize(applicationSettings);
	size += sizeof(quint32) + Serialize::stringSize(cDialogLastPath());

	// Theme keys and night mode.
	size += sizeof(quint32) + sizeof(quint64) * 2 + sizeof(quint32);
	size += sizeof(quint32) + sizeof(quint64) * 2;
	if (_langPackKey) {
		size += sizeof(quint32) + sizeof(quint64);
	}
	size += sizeof(quint32) + sizeof(qint32) * 8;

	const auto powerSaving = PowerSaving::Current().value();

	EncryptedDescriptor data(size);
	data.stream << quint32(dbiAutoStart) << qint32(cAutoStart());
	data.stream << quint32(dbiStartMinimized) << qint32(cStartMinimized());
	data.stream << quint32(dbiSendToMenu) << qint32(cSendToMenu());
	data.stream << quint32(dbiSeenTrayTooltip) << qint32(cSeenTrayTooltip());
	data.stream << quint32(dbiAutoUpdate) << qint32(cAutoUpdate());
	data.stream << quint32(dbiLastUpdateCheck) << qint32(cLastUpdateCheck());
	data.stream << quint32(dbiScalePercent) << qint32(cConfigScale());
	data.stream << quint32(dbiFallbackProductionConfig) << configSerialized;
	data.stream << quint32(dbiApplicationSettings) << applicationSettings;
	data.stream << quint32(dbiDialogLastPath) << cDialogLastPath();
	data.stream << quint32(dbiPowerSaving) << qint32(powerSaving);

	data.stream
		<< quint32(dbiThemeKey)
		<< quint64(_themeKeyDay)
		<< quint64(_themeKeyNight)
		<< quint32(Window::Theme::IsNightMode() ? 1 : 0);
	if (_useGlobalBackgroundKeys) {
		data.stream
			<< quint32(dbiBackgroundKey)
			<< quint64(_backgroundKeyDay)
			<< quint64(_backgroundKeyNight);
		data.stream
			<< quint32(dbiTileBackground)
			<< qint32(Window::Theme::Background()->tileDay() ? 1 : 0)
			<< qint32(Window::Theme::Background()->tileNight() ? 1 : 0);
	}
	if (_langPackKey) {
		data.stream << quint32(dbiLangPackKey) << quint64(_langPackKey);
	}
	if (_languagesKey) {
		data.stream << quint32(dbiLanguagesKey) << quint64(_languagesKey);
	}

	settings.writeEncrypted(data, SettingsKey);
}

void rewriteSettingsIfNeeded() {
	if (_settingsWriteAllowed) {
		return;
	}
	_settingsWriteAllowed = true;
	if (_oldSettingsVersion < AppVersion || _settingsRewriteNeeded) {
		writeSettings();
	}
}

const QString &AutoupdatePrefix(const QString &replaceWith = {}) {
	Expects(!Core::UpdaterDisabled());

	static auto value = QString();
	if (!replaceWith.isEmpty()) {
		value = replaceWith;
	}
	return value;
}

QString autoupdatePrefixFile() {
	Expects(!Core::UpdaterDisabled());

	return cWorkingDir() + "tdata/prefix";
}

const QString &readAutoupdatePrefixRaw() {
	Expects(!Core::UpdaterDisabled());

	const auto &result = AutoupdatePrefix();
	if (!result.isEmpty()) {
		return result;
	}
	QFile f(autoupdatePrefixFile());
	if (f.open(QIODevice::ReadOnly)) {
		const auto value = QString::fromUtf8(f.readAll());
		if (!value.isEmpty()) {
			return AutoupdatePrefix(value);
		}
	}
	return AutoupdatePrefix("https://td.telegram.org");
}

void writeAutoupdatePrefix(const QString &prefix) {
	if (Core::UpdaterDisabled()) {
		return;
	}

	const auto current = readAutoupdatePrefixRaw();
	if (current != prefix) {
		AutoupdatePrefix(prefix);
		QFile f(autoupdatePrefixFile());
		if (f.open(QIODevice::WriteOnly)) {
			f.write(prefix.toUtf8());
			f.close();
		}
		if (cAutoUpdate()) {
			Core::UpdateChecker checker;
			checker.start();
		}
	}
}

QString readAutoupdatePrefix() {
	Expects(!Core::UpdaterDisabled());

	auto result = readAutoupdatePrefixRaw();
	return result.replace(QRegularExpression("/+$"), QString());
}

void writeBackground(const Data::WallPaper &paper, const QImage &image) {
	Expects(_settingsWriteAllowed);

	if (!_backgroundCanWrite) {
		return;
	}

	_useGlobalBackgroundKeys = true;
	auto &backgroundKey = Window::Theme::IsNightMode()
		? _backgroundKeyNight
		: _backgroundKeyDay;
	auto imageData = QByteArray();
	if (!image.isNull()) {
		const auto width = qint32(image.width());
		const auto height = qint32(image.height());
		const auto perpixel = (image.depth() >> 3);
		const auto srcperline = image.bytesPerLine();
		const auto srcsize = srcperline * height;
		const auto dstperline = width * perpixel;
		const auto dstsize = dstperline * height;
		const auto copy = (image.format() != kSavedBackgroundFormat)
			? image.convertToFormat(kSavedBackgroundFormat)
			: image;
		imageData.resize(2 * sizeof(qint32) + dstsize);

		auto dst = bytes::make_detached_span(imageData);
		bytes::copy(dst, bytes::object_as_span(&width));
		dst = dst.subspan(sizeof(qint32));
		bytes::copy(dst, bytes::object_as_span(&height));
		dst = dst.subspan(sizeof(qint32));
		const auto src = bytes::make_span(image.constBits(), srcsize);
		if (srcsize == dstsize) {
			bytes::copy(dst, src);
		} else {
			for (auto y = 0; y != height; ++y) {
				bytes::copy(dst, src.subspan(y * srcperline, dstperline));
				dst = dst.subspan(dstperline);
			}
		}
	}
	if (!backgroundKey) {
		backgroundKey = GenerateKey(_basePath);
		writeSettings();
	}
	const auto serialized = paper.serialize();
	quint32 size = sizeof(qint32)
		+ Serialize::bytearraySize(serialized)
		+ Serialize::bytearraySize(imageData);
	EncryptedDescriptor data(size);
	data.stream
		<< qint32(kWallPaperSerializeTagId)
		<< serialized
		<< imageData;

	FileWriteDescriptor file(backgroundKey, _basePath);
	file.writeEncrypted(data, SettingsKey);
}

bool readBackground() {
	FileReadDescriptor bg;
	auto &backgroundKey = Window::Theme::IsNightMode()
		? _backgroundKeyNight
		: _backgroundKeyDay;
	if (!ReadEncryptedFile(bg, backgroundKey, _basePath, SettingsKey)) {
		if (backgroundKey) {
			ClearKey(backgroundKey, _basePath);
			backgroundKey = 0;
			writeSettings();
		}
		return false;
	}

	qint32 legacyId = 0;
	bg.stream >> legacyId;
	const auto paper = [&] {
		if (legacyId == kWallPaperLegacySerializeTagId) {
			quint64 id = 0;
			quint64 accessHash = 0;
			quint32 flags = 0;
			QString slug;
			bg.stream
				>> id
				>> accessHash
				>> flags
				>> slug;
			return Data::WallPaper::FromLegacySerialized(
				id,
				accessHash,
				flags,
				slug);
		} else if (legacyId == kWallPaperSerializeTagId) {
			QByteArray serialized;
			bg.stream >> serialized;
			return Data::WallPaper::FromSerialized(serialized);
		} else {
			return Data::WallPaper::FromLegacyId(legacyId);
		}
	}();
	if (bg.stream.status() != QDataStream::Ok || !paper) {
		return false;
	}

	QByteArray imageData;
	bg.stream >> imageData;
	const auto isOldEmptyImage = (bg.stream.status() != QDataStream::Ok);
	if (isOldEmptyImage
		|| Data::IsLegacy1DefaultWallPaper(*paper)
		|| (Data::IsLegacy2DefaultWallPaper(*paper) && bg.version < 3000000)
		|| (Data::IsLegacy3DefaultWallPaper(*paper) && bg.version < 3000000)
		|| (Data::IsLegacy4DefaultWallPaper(*paper) && bg.version < 3000000)
		|| Data::IsDefaultWallPaper(*paper)) {
		_backgroundCanWrite = false;
		if (isOldEmptyImage || bg.version < 3000000) {
			Window::Theme::Background()->set(Data::DefaultWallPaper());
		} else {
			Window::Theme::Background()->set(*paper);
		}
		_backgroundCanWrite = true;
		return true;
	} else if (Data::IsThemeWallPaper(*paper) && imageData.isEmpty()) {
		_backgroundCanWrite = false;
		Window::Theme::Background()->set(*paper);
		_backgroundCanWrite = true;
		return true;
	}
	auto image = QImage();
	if (legacyId == kWallPaperSerializeTagId) {
		const auto perpixel = 4;
		auto src = bytes::make_span(imageData);
		auto width = qint32();
		auto height = qint32();
		if (src.size() > 2 * sizeof(qint32)) {
			bytes::copy(
				bytes::object_as_span(&width),
				src.subspan(0, sizeof(qint32)));
			src = src.subspan(sizeof(qint32));
			bytes::copy(
				bytes::object_as_span(&height),
				src.subspan(0, sizeof(qint32)));
			src = src.subspan(sizeof(qint32));
			if (width + height <= kWallPaperSidesLimit
				&& src.size() == width * height * perpixel) {
				image = QImage(
					width,
					height,
					QImage::Format_ARGB32_Premultiplied);
				if (!image.isNull()) {
					const auto srcperline = width * perpixel;
					const auto srcsize = srcperline * height;
					const auto dstperline = image.bytesPerLine();
					const auto dstsize = dstperline * height;
					Assert(srcsize == dstsize);
					bytes::copy(
						bytes::make_span(image.bits(), dstsize),
						src);
				}
			}
		}
	} else {
		auto buffer = QBuffer(&imageData);
		auto reader = QImageReader(&buffer);
		reader.setAutoTransform(true);
		if (!reader.read(&image)) {
			image = QImage();
		}
	}
	if (!image.isNull() || !paper->backgroundColors().empty()) {
		_backgroundCanWrite = false;
		Window::Theme::Background()->set(*paper, std::move(image));
		_backgroundCanWrite = true;
		return true;
	}
	return false;
}

void moveLegacyBackground(
		const QString &fromBasePath,
		const MTP::AuthKeyPtr &fromLocalKey,
		uint64 legacyBackgroundKeyDay,
		uint64 legacyBackgroundKeyNight) {
	if (_useGlobalBackgroundKeys
		|| (!legacyBackgroundKeyDay && !legacyBackgroundKeyNight)) {
		return;
	}
	const auto move = [&](uint64 from, FileKey &to) {
		if (!from || to) {
			return;
		}
		to = GenerateKey(_basePath);
		FileReadDescriptor read;
		if (!ReadEncryptedFile(read, from, fromBasePath, fromLocalKey)) {
			return;
		}
		EncryptedDescriptor data;
		data.data = read.data;
		FileWriteDescriptor write(to, _basePath);
		write.writeEncrypted(data, SettingsKey);
	};
	move(legacyBackgroundKeyDay, _backgroundKeyDay);
	move(legacyBackgroundKeyNight, _backgroundKeyNight);
	_useGlobalBackgroundKeys = true;
	_settingsRewriteNeeded = true;
}

void reset() {
	if (_localLoader) {
		_localLoader->stop();
	}

	Window::Theme::Background()->reset();
	_oldSettingsVersion = 0;
	Core::App().settings().resetOnLastLogout();
	writeSettings();
}

int32 oldSettingsVersion() {
	return _oldSettingsVersion;
}

class CountWaveformTask : public Task {
public:
	CountWaveformTask(not_null<Data::DocumentMedia*> media)
	: _doc(media->owner())
	, _loc(_doc->location(true))
	, _data(media->bytes())
	, _wavemax(0) {
		if (_data.isEmpty() && !_loc.accessEnable()) {
			_doc = nullptr;
		}
	}
	void process() override {
		if (!_doc) return;

		_waveform = audioCountWaveform(_loc, _data);
		_wavemax = _waveform.empty()
			? char(0)
			: *ranges::max_element(_waveform);
	}
	void finish() override {
		if (const auto voice = _doc ? _doc->voice() : nullptr) {
			if (!_waveform.isEmpty()) {
				voice->waveform = _waveform;
				voice->wavemax = _wavemax;
			}
			if (voice->waveform.isEmpty()) {
				voice->waveform.resize(1);
				voice->waveform[0] = -2;
				voice->wavemax = 0;
			} else if (voice->waveform[0] < 0) {
				voice->waveform[0] = -2;
				voice->wavemax = 0;
			}
			_doc->owner().requestDocumentViewRepaint(_doc);
		}
	}
	~CountWaveformTask() {
		if (_data.isEmpty() && _doc) {
			_loc.accessDisable();
		}
	}

protected:
	DocumentData *_doc = nullptr;
	Core::FileLocation _loc;
	QByteArray _data;
	VoiceWaveform _waveform;
	char _wavemax;

};

void countVoiceWaveform(not_null<Data::DocumentMedia*> media) {
	const auto document = media->owner();
	if (const auto voice = document->voice()) {
		if (_localLoader) {
			voice->waveform.resize(1 + sizeof(TaskId));
			voice->waveform[0] = -1; // counting
			TaskId taskId = _localLoader->addTask(
				std::make_unique<CountWaveformTask>(media));
			memcpy(voice->waveform.data() + 1, &taskId, sizeof(taskId));
		}
	}
}

void cancelTask(TaskId id) {
	if (_localLoader) {
		_localLoader->cancelTask(id);
	}
}

Window::Theme::Saved readThemeUsingKey(FileKey key) {
	using namespace Window::Theme;

	FileReadDescriptor theme;
	if (!ReadEncryptedFile(theme, key, _basePath, SettingsKey)) {
		DEBUG_LOG(("Theme: Could not read file for key: %1").arg(key));
		return {};
	}

	auto tag = QString();
	auto result = Saved();
	auto &object = result.object;
	auto &cache = result.cache;
	auto field1 = qint32();
	auto field2 = quint32();
	theme.stream >> object.content;
	theme.stream >> tag >> object.pathAbsolute;
	if (tag == kThemeNewPathRelativeTag) {
		theme.stream
			>> object.pathRelative
			>> object.cloud.id
			>> object.cloud.accessHash
			>> object.cloud.slug
			>> object.cloud.title
			>> object.cloud.documentId
			>> field1;
	} else {
		object.pathRelative = tag;
	}
	if (theme.stream.status() != QDataStream::Ok) {
		DEBUG_LOG(("Theme: Bad status for key: %1, tag: %2"
			).arg(key
			).arg(tag));
		return {};
	}

	auto ignoreCache = false;
	if (!object.cloud.id) {
		auto file = QFile(object.pathRelative);
		if (object.pathRelative.isEmpty() || !file.exists()) {
			file.setFileName(object.pathAbsolute);
		}
		if (!file.fileName().isEmpty()
			&& file.exists()
			&& file.open(QIODevice::ReadOnly)) {
			if (file.size() > kThemeFileSizeLimit) {
				LOG(("Error: theme file too large: %1 "
					"(should be less than 5 MB, got %2)"
					).arg(file.fileName()
					).arg(file.size()));
				return {};
			}
			auto fileContent = file.readAll();
			file.close();
			if (object.content != fileContent) {
				object.content = fileContent;
				ignoreCache = true;
			}
		}
	}
	int32 cachePaletteChecksum = 0;
	int32 cacheContentChecksum = 0;
	QByteArray cacheColors;
	QByteArray cacheBackground;
	theme.stream
		>> cachePaletteChecksum
		>> cacheContentChecksum
		>> cacheColors
		>> cacheBackground
		>> field2;
	if (!ignoreCache) {
		if (theme.stream.status() != QDataStream::Ok) {
			DEBUG_LOG(("Theme: Bad status for cache, key: %1, tag: %2"
				).arg(key
				).arg(tag));
			return {};
		}
		cache.paletteChecksum = cachePaletteChecksum;
		cache.contentChecksum = cacheContentChecksum;
		cache.colors = std::move(cacheColors);
		cache.background = std::move(cacheBackground);
		cache.tiled = ((field2 & quint32(0xFF)) == 1);
	}
	if (tag == kThemeNewPathRelativeTag) {
		object.cloud.createdBy = UserId(
			((quint64(field2) >> 8) << 32) | quint64(quint32(field1)));
	}
	return result;
}

std::optional<QString> InitialLoadThemeUsingKey(FileKey key) {
	auto read = readThemeUsingKey(key);
	const auto result = read.object.pathAbsolute;
	if (read.object.content.isEmpty()) {
		DEBUG_LOG(("Theme: Could not read content for key: %1").arg(key));
	}
	if (read.object.content.isEmpty()
		|| !Window::Theme::Initialize(std::move(read))) {
		DEBUG_LOG(("Theme: Could not initialized for key: %1").arg(key));
		return std::nullopt;
	}
	return result;
}

void writeTheme(const Window::Theme::Saved &saved) {
	using namespace Window::Theme;

	if (_themeKeyLegacy) {
		DEBUG_LOG(("Theme: skipping write, because legacy: %1"
			).arg(_themeKeyLegacy));
		return;
	}
	auto &themeKey = IsNightMode()
		? _themeKeyNight
		: _themeKeyDay;
	DEBUG_LOG(("Theme: writing (night: %1), key_day: %2, key_night: %3"
		).arg(Logs::b(IsNightMode())
		).arg(_themeKeyDay
		).arg(_themeKeyNight));
	if (saved.object.content.isEmpty()) {
		if (themeKey) {
			if (IsNightMode()) {
				DEBUG_LOG(("Theme: cleared for night mode."));
				SetNightModeValue(false);
			}
			ClearKey(themeKey, _basePath);
			themeKey = 0;
			writeSettings();
		}
		return;
	}

	if (!themeKey) {
		themeKey = GenerateKey(_basePath);
		writeSettings();
	}

	const auto &object = saved.object;
	const auto &cache = saved.cache;
	const auto tag = QString(kThemeNewPathRelativeTag);
	quint32 size = Serialize::bytearraySize(object.content)
		+ Serialize::stringSize(tag)
		+ Serialize::stringSize(object.pathAbsolute)
		+ Serialize::stringSize(object.pathRelative)
		+ sizeof(uint64) * 3
		+ Serialize::stringSize(object.cloud.slug)
		+ Serialize::stringSize(object.cloud.title)
		+ sizeof(qint32)
		+ sizeof(qint32) * 2
		+ Serialize::bytearraySize(cache.colors)
		+ Serialize::bytearraySize(cache.background)
		+ sizeof(quint32);
	const auto bareCreatedById = object.cloud.createdBy.bare;
	Assert((bareCreatedById & PeerId::kChatTypeMask) == bareCreatedById);
	const auto field1 = qint32(quint32(bareCreatedById & 0xFFFFFFFFULL));
	const auto field2 = quint32(cache.tiled ? 1 : 0)
		| (quint32(bareCreatedById >> 32) << 8);
	EncryptedDescriptor data(size);
	data.stream
		<< object.content
		<< tag
		<< object.pathAbsolute
		<< object.pathRelative
		<< object.cloud.id
		<< object.cloud.accessHash
		<< object.cloud.slug
		<< object.cloud.title
		<< object.cloud.documentId
		<< field1
		<< cache.paletteChecksum
		<< cache.contentChecksum
		<< cache.colors
		<< cache.background
		<< field2;

	FileWriteDescriptor file(themeKey, _basePath);
	file.writeEncrypted(data, SettingsKey);
}

void clearTheme() {
	writeTheme(Window::Theme::Saved());
}

void InitialLoadTheme() {
	const auto key = (_themeKeyLegacy != 0)
		? _themeKeyLegacy
		: (Window::Theme::IsNightMode()
			? _themeKeyNight
			: _themeKeyDay);
	DEBUG_LOG(("Theme: initial load (night: %1), "
		"key_legacy: %2, key_day: %3, key_night: %4"
		).arg(Logs::b(Window::Theme::IsNightMode())
		).arg(_themeKeyLegacy
		).arg(_themeKeyDay
		).arg(_themeKeyNight));
	if (!key) {
		if (Window::Theme::IsNightMode()) {
			DEBUG_LOG(("Theme: zero key for night mode."));
			Window::Theme::SetNightModeValue(false);
		}
		return;
	} else if (const auto path = InitialLoadThemeUsingKey(key)) {
		DEBUG_LOG(("Theme: loaded with result: %1").arg(*path));
		if (_themeKeyLegacy) {
			Window::Theme::SetNightModeValue(*path
				== Window::Theme::NightThemePath());
			(Window::Theme::IsNightMode()
				? _themeKeyNight
				: _themeKeyDay) = base::take(_themeKeyLegacy);
			DEBUG_LOG(("Theme: now (night: %1), "
				"key_legacy: %2, key_day: %3, key_night: %4 (path: %5)"
				).arg(Logs::b(Window::Theme::IsNightMode())
				).arg(_themeKeyLegacy
				).arg(_themeKeyDay
				).arg(_themeKeyNight
				).arg(*path));
		}
	} else {
		DEBUG_LOG(("Theme: could not load, clearing.."));
		clearTheme();
	}
}

bool ApplyDefaultNightMode() {
	const auto NightByDefault = Platform::IsMacStoreBuild();
	if (!NightByDefault
		|| Window::Theme::IsNightMode()
		|| _themeKeyDay
		|| _themeKeyNight
		|| _themeKeyLegacy) {
		return false;
	}
	Core::App().startSettingsAndBackground();
	Window::Theme::ToggleNightMode();
	Window::Theme::KeepApplied();
	return true;
}

Window::Theme::Saved readThemeAfterSwitch() {
	const auto key = Window::Theme::IsNightMode()
		? _themeKeyNight
		: _themeKeyDay;
	return readThemeUsingKey(key);
}

void readLangPack() {
	FileReadDescriptor langpack;
	if (!_langPackKey || !ReadEncryptedFile(langpack, _langPackKey, _basePath, SettingsKey)) {
		return;
	}
	auto data = QByteArray();
	langpack.stream >> data;
	if (langpack.stream.status() == QDataStream::Ok) {
		Lang::GetInstance().fillFromSerialized(data, langpack.version);
	}
}

void writeLangPack() {
	auto langpack = Lang::GetInstance().serialize();
	if (!_langPackKey) {
		_langPackKey = GenerateKey(_basePath);
		writeSettings();
	}

	EncryptedDescriptor data(Serialize::bytearraySize(langpack));
	data.stream << langpack;

	FileWriteDescriptor file(_langPackKey, _basePath);
	file.writeEncrypted(data, SettingsKey);
}

void saveRecentLanguages(const std::vector<Lang::Language> &list) {
	if (list.empty()) {
		if (_languagesKey) {
			ClearKey(_languagesKey, _basePath);
			_languagesKey = 0;
			writeSettings();
		}
		return;
	}

	auto size = sizeof(qint32);
	for (const auto &language : list) {
		size += Serialize::stringSize(language.id)
			+ Serialize::stringSize(language.pluralId)
			+ Serialize::stringSize(language.baseId)
			+ Serialize::stringSize(language.name)
			+ Serialize::stringSize(language.nativeName);
	}
	if (!_languagesKey) {
		_languagesKey = GenerateKey(_basePath);
		writeSettings();
	}

	EncryptedDescriptor data(size);
	data.stream << qint32(list.size());
	for (const auto &language : list) {
		data.stream
			<< language.id
			<< language.pluralId
			<< language.baseId
			<< language.name
			<< language.nativeName;
	}

	FileWriteDescriptor file(_languagesKey, _basePath);
	file.writeEncrypted(data, SettingsKey);
}

void pushRecentLanguage(const Lang::Language &language) {
	if (language.id.startsWith('#')) {
		return;
	}
	auto list = readRecentLanguages();
	list.erase(
		ranges::remove_if(
			list,
			[&](const Lang::Language &v) { return (v.id == language.id); }),
		end(list));
	list.insert(list.begin(), language);

	saveRecentLanguages(list);
}

void removeRecentLanguage(const QString &id) {
	auto list = readRecentLanguages();
	list.erase(
		ranges::remove_if(
			list,
			[&](const Lang::Language &v) { return (v.id == id); }),
		end(list));

	saveRecentLanguages(list);
}

std::vector<Lang::Language> readRecentLanguages() {
	FileReadDescriptor languages;
	if (!_languagesKey || !ReadEncryptedFile(languages, _languagesKey, _basePath, SettingsKey)) {
		return {};
	}
	qint32 count = 0;
	languages.stream >> count;
	if (count <= 0) {
		return {};
	}
	auto result = std::vector<Lang::Language>();
	result.reserve(count);
	for (auto i = 0; i != count; ++i) {
		auto language = Lang::Language();
		languages.stream
			>> language.id
			>> language.pluralId
			>> language.baseId
			>> language.name
			>> language.nativeName;
		result.push_back(language);
	}
	if (languages.stream.status() != QDataStream::Ok) {
		return {};
	}
	return result;
}

Window::Theme::Object ReadThemeContent() {
	using namespace Window::Theme;

	auto &themeKey = IsNightMode() ? _themeKeyNight : _themeKeyDay;
	if (!themeKey) {
		return Object();
	}

	FileReadDescriptor theme;
	if (!ReadEncryptedFile(theme, themeKey, _basePath, SettingsKey)) {
		return Object();
	}

	QByteArray content;
	QString pathRelative, pathAbsolute;
	theme.stream >> content >> pathRelative >> pathAbsolute;
	if (theme.stream.status() != QDataStream::Ok) {
		return Object();
	}

	auto result = Object();
	result.pathAbsolute = pathAbsolute;
	result.content = content;
	return result;
}

void incrementRecentHashtag(RecentHashtagPack &recent, const QString &tag) {
	auto i = recent.begin(), e = recent.end();
	for (; i != e; ++i) {
		if (i->first == tag) {
			++i->second;
			if (qAbs(i->second) > 0x4000) {
				for (auto j = recent.begin(); j != e; ++j) {
					if (j->second > 1) {
						j->second /= 2;
					} else if (j->second > 0) {
						j->second = 1;
					}
				}
			}
			for (; i != recent.begin(); --i) {
				if (qAbs((i - 1)->second) > qAbs(i->second)) {
					break;
				}
				qSwap(*i, *(i - 1));
			}
			break;
		}
	}
	if (i == e) {
		while (recent.size() >= 64) recent.pop_back();
		recent.push_back(qMakePair(tag, 1));
		for (i = recent.end() - 1; i != recent.begin(); --i) {
			if ((i - 1)->second > i->second) {
				break;
			}
			qSwap(*i, *(i - 1));
		}
	}
}

bool readOldMtpData(bool remove, ReadSettingsContext &context) {
	return _readOldMtpData(remove, context);
}

bool readOldUserSettings(bool remove, ReadSettingsContext &context) {
	return _readOldUserSettings(remove, context);
}

} // namespace Local
