/*
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_document.h"
#include "storage/serialize_common.h"
#include "storage/storage_encrypted_file.h"
#include "storage/storage_clear_legacy.h"
#include "chat_helpers/stickers.h"
#include "data/data_drafts.h"
#include "boxes/send_files_box.h"
#include "window/themes/window_theme.h"
#include "ui/widgets/input_fields.h"
#include "ui/emoji_config.h"
#include "export/export_settings.h"
#include "core/crash_reports.h"
#include "core/update_checker.h"
#include "observer_peer.h"
#include "mainwidget.h"
#include "mainwindow.h"
#include "lang/lang_keys.h"
#include "lang/lang_cloud_manager.h"
#include "media/media_audio.h"
#include "mtproto/dc_options.h"
#include "messenger.h"
#include "application.h"
#include "apiwrap.h"
#include "auth_session.h"
#include "window/window_controller.h"
#include "base/flags.h"
#include "data/data_session.h"
#include "history/history.h"

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

namespace Local {
namespace {

constexpr auto kThemeFileSizeLimit = 5 * 1024 * 1024;
constexpr auto kFileLoaderQueueStopTimeout = TimeMs(5000);
constexpr auto kDefaultStickerInstallDate = TimeId(1);
constexpr auto kProxyTypeShift = 1024;

constexpr auto kSinglePeerTypeUser = qint32(1);
constexpr auto kSinglePeerTypeChat = qint32(2);
constexpr auto kSinglePeerTypeChannel = qint32(3);
constexpr auto kSinglePeerTypeSelf = qint32(4);
constexpr auto kSinglePeerTypeEmpty = qint32(0);

using Database = Storage::Cache::Database;
using FileKey = quint64;

constexpr char tdfMagic[] = { 'T', 'D', 'F', '$' };
constexpr auto tdfMagicLen = int(sizeof(tdfMagic));

QString toFilePart(FileKey val) {
	QString result;
	result.reserve(0x10);
	for (int32 i = 0; i < 0x10; ++i) {
		uchar v = (val & 0x0F);
		result.push_back((v < 0x0A) ? ('0' + v) : ('A' + (v - 0x0A)));
		val >>= 4;
	}
	return result;
}

QString _basePath, _userBasePath, _userDbPath;

bool _started = false;
internal::Manager *_manager = nullptr;
TaskQueue *_localLoader = nullptr;

bool _working() {
	return _manager && !_basePath.isEmpty();
}

bool _userWorking() {
	return _manager && !_basePath.isEmpty() && !_userBasePath.isEmpty();
}

enum class FileOption {
	User = (1 << 0),
	Safe = (1 << 1),
};
using FileOptions = base::flags<FileOption>;
inline constexpr auto is_flag_type(FileOption) { return true; };

bool keyAlreadyUsed(QString &name, FileOptions options = FileOption::User | FileOption::Safe) {
	name += '0';
	if (QFileInfo(name).exists()) return true;
	if (options & (FileOption::Safe)) {
		name[name.size() - 1] = '1';
		return QFileInfo(name).exists();
	}
	return false;
}

FileKey genKey(FileOptions options = FileOption::User | FileOption::Safe) {
	if (options & FileOption::User) {
		if (!_userWorking()) return 0;
	} else {
		if (!_working()) return 0;
	}

	FileKey result;
	QString base = (options & FileOption::User) ? _userBasePath : _basePath, path;
	path.reserve(base.size() + 0x11);
	path += base;
	do {
		result = rand_value<FileKey>();
		path.resize(base.size());
		path += toFilePart(result);
	} while (!result || keyAlreadyUsed(path, options));

	return result;
}

void clearKey(const FileKey &key, FileOptions options = FileOption::User | FileOption::Safe) {
	if (options & FileOption::User) {
		if (!_userWorking()) return;
	} else {
		if (!_working()) return;
	}

	QString base = (options & FileOption::User) ? _userBasePath : _basePath, name;
	name.reserve(base.size() + 0x11);
	name.append(base).append(toFilePart(key)).append('0');
	QFile::remove(name);
	if (options & FileOption::Safe) {
		name[name.size() - 1] = '1';
		QFile::remove(name);
	}
}

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

QByteArray _settingsSalt, _passKeySalt, _passKeyEncrypted;

constexpr auto kLocalKeySize = MTP::AuthKey::kSize;

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

void createLocalKey(const QByteArray &pass, QByteArray *salt, MTP::AuthKeyPtr *result) {
	auto key = MTP::AuthKey::Data { { gsl::byte{} } };
	auto iterCount = pass.size() ? LocalEncryptIterCount : LocalEncryptNoPwdIterCount; // dont slow down for no password
	auto newSalt = QByteArray();
	if (!salt) {
		newSalt.resize(LocalEncryptSaltSize);
		memset_rand(newSalt.data(), newSalt.size());
		salt = &newSalt;

		cSetLocalSalt(newSalt);
	}

	PKCS5_PBKDF2_HMAC_SHA1(pass.constData(), pass.size(), (uchar*)salt->data(), salt->size(), iterCount, key.size(), (uchar*)key.data());

	*result = std::make_shared<MTP::AuthKey>(key);
}

struct FileReadDescriptor {
	int32 version = 0;
	QByteArray data;
	QBuffer buffer;
	QDataStream stream;
	~FileReadDescriptor() {
		if (version) {
			stream.setDevice(0);
			if (buffer.isOpen()) buffer.close();
			buffer.setBuffer(0);
		}
	}
};

struct EncryptedDescriptor {
	EncryptedDescriptor() {
	}
	EncryptedDescriptor(uint32 size) {
		uint32 fullSize = sizeof(uint32) + size;
		if (fullSize & 0x0F) fullSize += 0x10 - (fullSize & 0x0F);
		data.reserve(fullSize);

		data.resize(sizeof(uint32));
		buffer.setBuffer(&data);
		buffer.open(QIODevice::WriteOnly);
		buffer.seek(sizeof(uint32));
		stream.setDevice(&buffer);
		stream.setVersion(QDataStream::Qt_5_1);
	}
	QByteArray data;
	QBuffer buffer;
	QDataStream stream;
	void finish() {
		if (stream.device()) stream.setDevice(0);
		if (buffer.isOpen()) buffer.close();
		buffer.setBuffer(0);
	}
	~EncryptedDescriptor() {
		finish();
	}
};

struct FileWriteDescriptor {
	FileWriteDescriptor(const FileKey &key, FileOptions options = FileOption::User | FileOption::Safe) {
		init(toFilePart(key), options);
	}
	FileWriteDescriptor(const QString &name, FileOptions options = FileOption::User | FileOption::Safe) {
		init(name, options);
	}
	void init(const QString &name, FileOptions options) {
		if (options & FileOption::User) {
			if (!_userWorking()) return;
		} else {
			if (!_working()) return;
		}

		// detect order of read attempts and file version
		QString toTry[2];
		toTry[0] = ((options & FileOption::User) ? _userBasePath : _basePath) + name + '0';
		if (options & FileOption::Safe) {
			toTry[1] = ((options & FileOption::User) ? _userBasePath : _basePath) + name + '1';
			QFileInfo toTry0(toTry[0]);
			QFileInfo toTry1(toTry[1]);
			if (toTry0.exists()) {
				if (toTry1.exists()) {
					QDateTime mod0 = toTry0.lastModified(), mod1 = toTry1.lastModified();
					if (mod0 > mod1) {
						qSwap(toTry[0], toTry[1]);
					}
				} else {
					qSwap(toTry[0], toTry[1]);
				}
				toDelete = toTry[1];
			} else if (toTry1.exists()) {
				toDelete = toTry[1];
			}
		}

		file.setFileName(toTry[0]);
		if (file.open(QIODevice::WriteOnly)) {
			file.write(tdfMagic, tdfMagicLen);
			qint32 version = AppVersion;
			file.write((const char*)&version, sizeof(version));

			stream.setDevice(&file);
			stream.setVersion(QDataStream::Qt_5_1);
		}
	}
	bool writeData(const QByteArray &data) {
		if (!file.isOpen()) return false;

		stream << data;
		quint32 len = data.isNull() ? 0xffffffff : data.size();
		if (QSysInfo::ByteOrder != QSysInfo::BigEndian) {
			len = qbswap(len);
		}
		md5.feed(&len, sizeof(len));
		md5.feed(data.constData(), data.size());
		dataSize += sizeof(len) + data.size();

		return true;
	}
	static QByteArray prepareEncrypted(EncryptedDescriptor &data, const MTP::AuthKeyPtr &key = LocalKey) {
		data.finish();
		QByteArray &toEncrypt(data.data);

		// prepare for encryption
		uint32 size = toEncrypt.size(), fullSize = size;
		if (fullSize & 0x0F) {
			fullSize += 0x10 - (fullSize & 0x0F);
			toEncrypt.resize(fullSize);
			memset_rand(toEncrypt.data() + size, fullSize - size);
		}
		*(uint32*)toEncrypt.data() = size;
		QByteArray encrypted(0x10 + fullSize, Qt::Uninitialized); // 128bit of sha1 - key128, sizeof(data), data
		hashSha1(toEncrypt.constData(), toEncrypt.size(), encrypted.data());
		MTP::aesEncryptLocal(toEncrypt.constData(), encrypted.data() + 0x10, fullSize, key, encrypted.constData());

		return encrypted;
	}
	bool writeEncrypted(EncryptedDescriptor &data, const MTP::AuthKeyPtr &key = LocalKey) {
		return writeData(prepareEncrypted(data, key));
	}
	void finish() {
		if (!file.isOpen()) return;

		stream.setDevice(0);

		md5.feed(&dataSize, sizeof(dataSize));
		qint32 version = AppVersion;
		md5.feed(&version, sizeof(version));
		md5.feed(tdfMagic, tdfMagicLen);
		file.write((const char*)md5.result(), 0x10);
		file.close();

		if (!toDelete.isEmpty()) {
			QFile::remove(toDelete);
		}
	}
	QFile file;
	QDataStream stream;

	QString toDelete;

	HashMd5 md5;
	int32 dataSize = 0;

	~FileWriteDescriptor() {
		finish();
	}
};

bool readFile(FileReadDescriptor &result, const QString &name, FileOptions options = FileOption::User | FileOption::Safe) {
	if (options & FileOption::User) {
		if (!_userWorking()) return false;
	} else {
		if (!_working()) return false;
	}

	// detect order of read attempts
	QString toTry[2];
	toTry[0] = ((options & FileOption::User) ? _userBasePath : _basePath) + name + '0';
	if (options & FileOption::Safe) {
		QFileInfo toTry0(toTry[0]);
		if (toTry0.exists()) {
			toTry[1] = ((options & FileOption::User) ? _userBasePath : _basePath) + name + '1';
			QFileInfo toTry1(toTry[1]);
			if (toTry1.exists()) {
				QDateTime mod0 = toTry0.lastModified(), mod1 = toTry1.lastModified();
				if (mod0 < mod1) {
					qSwap(toTry[0], toTry[1]);
				}
			} else {
				toTry[1] = QString();
			}
		} else {
			toTry[0][toTry[0].size() - 1] = '1';
		}
	}
	for (int32 i = 0; i < 2; ++i) {
		QString fname(toTry[i]);
		if (fname.isEmpty()) break;

		QFile f(fname);
		if (!f.open(QIODevice::ReadOnly)) {
			DEBUG_LOG(("App Info: failed to open '%1' for reading").arg(name));
			continue;
		}

		// check magic
		char magic[tdfMagicLen];
		if (f.read(magic, tdfMagicLen) != tdfMagicLen) {
			DEBUG_LOG(("App Info: failed to read magic from '%1'").arg(name));
			continue;
		}
		if (memcmp(magic, tdfMagic, tdfMagicLen)) {
			DEBUG_LOG(("App Info: bad magic %1 in '%2'").arg(Logs::mb(magic, tdfMagicLen).str()).arg(name));
			continue;
		}

		// read app version
		qint32 version;
		if (f.read((char*)&version, sizeof(version)) != sizeof(version)) {
			DEBUG_LOG(("App Info: failed to read version from '%1'").arg(name));
			continue;
		}
		if (version > AppVersion) {
			DEBUG_LOG(("App Info: version too big %1 for '%2', my version %3").arg(version).arg(name).arg(AppVersion));
			continue;
		}

		// read data
		QByteArray bytes = f.read(f.size());
		int32 dataSize = bytes.size() - 16;
		if (dataSize < 0) {
			DEBUG_LOG(("App Info: bad file '%1', could not read sign part").arg(name));
			continue;
		}

		// check signature
		HashMd5 md5;
		md5.feed(bytes.constData(), dataSize);
		md5.feed(&dataSize, sizeof(dataSize));
		md5.feed(&version, sizeof(version));
		md5.feed(magic, tdfMagicLen);
		if (memcmp(md5.result(), bytes.constData() + dataSize, 16)) {
			DEBUG_LOG(("App Info: bad file '%1', signature did not match").arg(name));
			continue;
		}

		bytes.resize(dataSize);
		result.data = bytes;
		bytes = QByteArray();

		result.version = version;
		result.buffer.setBuffer(&result.data);
		result.buffer.open(QIODevice::ReadOnly);
		result.stream.setDevice(&result.buffer);
		result.stream.setVersion(QDataStream::Qt_5_1);

		if ((i == 0 && !toTry[1].isEmpty()) || i == 1) {
			QFile::remove(toTry[1 - i]);
		}

		return true;
	}
	return false;
}

bool decryptLocal(EncryptedDescriptor &result, const QByteArray &encrypted, const MTP::AuthKeyPtr &key = LocalKey) {
	if (encrypted.size() <= 16 || (encrypted.size() & 0x0F)) {
		LOG(("App Error: bad encrypted part size: %1").arg(encrypted.size()));
		return false;
	}
	uint32 fullLen = encrypted.size() - 16;

	QByteArray decrypted;
	decrypted.resize(fullLen);
	const char *encryptedKey = encrypted.constData(), *encryptedData = encrypted.constData() + 16;
	aesDecryptLocal(encryptedData, decrypted.data(), fullLen, key, encryptedKey);
	uchar sha1Buffer[20];
	if (memcmp(hashSha1(decrypted.constData(), decrypted.size(), sha1Buffer), encryptedKey, 16)) {
		LOG(("App Info: bad decrypt key, data not decrypted - incorrect password?"));
		return false;
	}

	uint32 dataLen = *(const uint32*)decrypted.constData();
	if (dataLen > uint32(decrypted.size()) || dataLen <= fullLen - 16 || dataLen < sizeof(uint32)) {
		LOG(("App Error: bad decrypted part size: %1, fullLen: %2, decrypted size: %3").arg(dataLen).arg(fullLen).arg(decrypted.size()));
		return false;
	}

	decrypted.resize(dataLen);
	result.data = decrypted;
	decrypted = QByteArray();

	result.buffer.setBuffer(&result.data);
	result.buffer.open(QIODevice::ReadOnly);
	result.buffer.seek(sizeof(uint32)); // skip len
	result.stream.setDevice(&result.buffer);
	result.stream.setVersion(QDataStream::Qt_5_1);

	return true;
}

bool readEncryptedFile(FileReadDescriptor &result, const QString &name, FileOptions options = FileOption::User | FileOption::Safe, const MTP::AuthKeyPtr &key = LocalKey) {
	if (!readFile(result, name, options)) {
		return false;
	}
	QByteArray encrypted;
	result.stream >> encrypted;

	EncryptedDescriptor data;
	if (!decryptLocal(data, encrypted, key)) {
		result.stream.setDevice(0);
		if (result.buffer.isOpen()) result.buffer.close();
		result.buffer.setBuffer(0);
		result.data = QByteArray();
		result.version = 0;
		return false;
	}

	result.stream.setDevice(0);
	if (result.buffer.isOpen()) result.buffer.close();
	result.buffer.setBuffer(0);
	result.data = data.data;
	result.buffer.setBuffer(&result.data);
	result.buffer.open(QIODevice::ReadOnly);
	result.buffer.seek(data.buffer.pos());
	result.stream.setDevice(&result.buffer);
	result.stream.setVersion(QDataStream::Qt_5_1);

	return true;
}

bool readEncryptedFile(FileReadDescriptor &result, const FileKey &fkey, FileOptions options = FileOption::User | FileOption::Safe, const MTP::AuthKeyPtr &key = LocalKey) {
	return readEncryptedFile(result, toFilePart(fkey), options, key);
}

FileKey _dataNameKey = 0;

enum { // Local Storage Keys
	lskUserMap = 0x00,
	lskDraft = 0x01, // data: PeerId peer
	lskDraftPosition = 0x02, // data: PeerId peer
	lskImages = 0x03, // data: StorageKey location
	lskLocations = 0x04, // no data
	lskStickerImages = 0x05, // data: StorageKey location
	lskAudios = 0x06, // data: StorageKey location
	lskRecentStickersOld = 0x07, // no data
	lskBackgroundOld = 0x08, // no data
	lskUserSettings = 0x09, // no data
	lskRecentHashtagsAndBots = 0x0a, // no data
	lskStickersOld = 0x0b, // no data
	lskSavedPeers = 0x0c, // no data
	lskReportSpamStatuses = 0x0d, // no data
	lskSavedGifsOld = 0x0e, // no data
	lskSavedGifs = 0x0f, // no data
	lskStickersKeys = 0x10, // no data
	lskTrustedBots = 0x11, // no data
	lskFavedStickers = 0x12, // no data
	lskExportSettings = 0x13, // no data
	lskBackground = 0x14, // no data
	lskSelfSerialized = 0x15, // serialized self
};

enum {
	dbiKey = 0x00,
	dbiUser = 0x01,
	dbiDcOptionOldOld = 0x02,
	dbiChatSizeMax = 0x03,
	dbiMutePeer = 0x04,
	dbiSendKeyOld = 0x05,
	dbiAutoStart = 0x06,
	dbiStartMinimized = 0x07,
	dbiSoundNotify = 0x08,
	dbiWorkMode = 0x09,
	dbiSeenTrayTooltip = 0x0a,
	dbiDesktopNotify = 0x0b,
	dbiAutoUpdate = 0x0c,
	dbiLastUpdateCheck = 0x0d,
	dbiWindowPosition = 0x0e,
	dbiConnectionTypeOld = 0x0f,
	// 0x10 reserved
	dbiDefaultAttach = 0x11,
	dbiCatsAndDogs = 0x12,
	dbiReplaceEmoji = 0x13,
	dbiAskDownloadPath = 0x14,
	dbiDownloadPathOld = 0x15,
	dbiScaleOld = 0x16,
	dbiEmojiTabOld = 0x17,
	dbiRecentEmojiOldOld = 0x18,
	dbiLoggedPhoneNumber = 0x19,
	dbiMutedPeers = 0x1a,
	// 0x1b reserved
	dbiNotifyView = 0x1c,
	dbiSendToMenu = 0x1d,
	dbiCompressPastedImage = 0x1e,
	dbiLangOld = 0x1f,
	dbiLangFileOld = 0x20,
	dbiTileBackgroundOld = 0x21,
	dbiAutoLock = 0x22,
	dbiDialogLastPath = 0x23,
	dbiRecentEmojiOld = 0x24,
	dbiEmojiVariantsOld = 0x25,
	dbiRecentStickers = 0x26,
	dbiDcOptionOld = 0x27,
	dbiTryIPv6 = 0x28,
	dbiSongVolume = 0x29,
	dbiWindowsNotificationsOld = 0x30,
	dbiIncludeMutedOld = 0x31,
	dbiMegagroupSizeMax = 0x32,
	dbiDownloadPath = 0x33,
	dbiAutoDownload = 0x34,
	dbiSavedGifsLimit = 0x35,
	dbiShowingSavedGifsOld = 0x36,
	dbiAutoPlay = 0x37,
	dbiAdaptiveForWide = 0x38,
	dbiHiddenPinnedMessages = 0x39,
	dbiRecentEmoji = 0x3a,
	dbiEmojiVariants = 0x3b,
	dbiDialogsMode = 0x40,
	dbiModerateMode = 0x41,
	dbiVideoVolume = 0x42,
	dbiStickersRecentLimit = 0x43,
	dbiNativeNotifications = 0x44,
	dbiNotificationsCount  = 0x45,
	dbiNotificationsCorner = 0x46,
	dbiThemeKeyOld = 0x47,
	dbiDialogsWidthRatioOld = 0x48,
	dbiUseExternalVideoPlayer = 0x49,
	dbiDcOptions = 0x4a,
	dbiMtpAuthorization = 0x4b,
	dbiLastSeenWarningSeenOld = 0x4c,
	dbiAuthSessionSettings = 0x4d,
	dbiLangPackKey = 0x4e,
	dbiConnectionType = 0x4f,
	dbiStickersFavedLimit = 0x50,
	dbiSuggestStickersByEmoji = 0x51,
	dbiSuggestEmoji = 0x52,
	dbiTxtDomainString = 0x53,
	dbiThemeKey = 0x54,
	dbiTileBackground = 0x55,
	dbiCacheSettings = 0x56,
	dbiAnimationsDisabled = 0x57,
	dbiScalePercent = 0x58,
	dbiPlaybackSpeed = 0x59,
	dbiLanguagesKey = 0x5a,

	dbiEncryptedWithSalt = 333,
	dbiEncrypted = 444,

	// 500-600 reserved

	dbiVersion = 666,
};

enum {
	dbictAuto = 0,
	dbictHttpAuto = 1, // not used
	dbictHttpProxy = 2,
	dbictTcpProxy = 3,
	dbictProxiesListOld = 4,
	dbictProxiesList = 5,
};

typedef QMap<PeerId, FileKey> DraftsMap;
DraftsMap _draftsMap, _draftCursorsMap;
typedef QMap<PeerId, bool> DraftsNotReadMap;
DraftsNotReadMap _draftsNotReadMap;

typedef QPair<FileKey, qint32> FileDesc; // file, size

typedef QMultiMap<MediaKey, FileLocation> FileLocations;
FileLocations _fileLocations;
typedef QPair<MediaKey, FileLocation> FileLocationPair;
typedef QMap<QString, FileLocationPair> FileLocationPairs;
FileLocationPairs _fileLocationPairs;
typedef QMap<MediaKey, MediaKey> FileLocationAliases;
FileLocationAliases _fileLocationAliases;
FileKey _locationsKey = 0, _reportSpamStatusesKey = 0, _trustedBotsKey = 0;

using TrustedBots = OrderedSet<uint64>;
TrustedBots _trustedBots;
bool _trustedBotsRead = false;

FileKey _recentStickersKeyOld = 0;
FileKey _installedStickersKey = 0, _featuredStickersKey = 0, _recentStickersKey = 0, _favedStickersKey = 0, _archivedStickersKey = 0;
FileKey _savedGifsKey = 0;

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

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 loadTheme()
// and never used after.
FileKey _themeKeyLegacy = 0;

bool _readingUserSettings = false;
FileKey _userSettingsKey = 0;
FileKey _recentHashtagsAndBotsKey = 0;
bool _recentHashtagsAndBotsWereRead = false;
qint64 _cacheTotalSizeLimit = Database::Settings().totalSizeLimit;
qint32 _cacheTotalTimeLimit = Database::Settings().totalTimeLimit;

FileKey _exportSettingsKey = 0;

FileKey _savedPeersKey = 0;
FileKey _langPackKey = 0;
FileKey _languagesKey = 0;

bool _mapChanged = false;
int32 _oldMapVersion = 0, _oldSettingsVersion = 0;

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

std::unique_ptr<AuthSessionSettings> StoredAuthSessionCache;
AuthSessionSettings &GetStoredAuthSessionCache() {
	if (!StoredAuthSessionCache) {
		StoredAuthSessionCache = std::make_unique<AuthSessionSettings>();
	}
	return *StoredAuthSessionCache;
}

void _writeMap(WriteMapWhen when = WriteMapWhen::Soon);

void _writeLocations(WriteMapWhen when = WriteMapWhen::Soon) {
	if (when != WriteMapWhen::Now) {
		_manager->writeLocations(when == WriteMapWhen::Fast);
		return;
	}
	if (!_working()) return;

	_manager->writingLocations();
	if (_fileLocations.isEmpty()) {
		if (_locationsKey) {
			clearKey(_locationsKey);
			_locationsKey = 0;
			_mapChanged = true;
			_writeMap();
		}
	} else {
		if (!_locationsKey) {
			_locationsKey = genKey();
			_mapChanged = true;
			_writeMap(WriteMapWhen::Fast);
		}
		quint32 size = 0;
		for (FileLocations::const_iterator i = _fileLocations.cbegin(), e = _fileLocations.cend(); i != e; ++i) {
			// location + type + namelen + name
			size += sizeof(quint64) * 2 + sizeof(quint32) + Serialize::stringSize(i.value().name());
			if (AppVersion > 9013) {
				// bookmark
				size += Serialize::bytearraySize(i.value().bookmark());
			}
			// date + size
			size += Serialize::dateTimeSize() + sizeof(quint32);
		}

		//end mark
		size += sizeof(quint64) * 2 + sizeof(quint32) + Serialize::stringSize(QString());
		if (AppVersion > 9013) {
			size += Serialize::bytearraySize(QByteArray());
		}
		size += Serialize::dateTimeSize() + sizeof(quint32);

		size += sizeof(quint32); // aliases count
		for (FileLocationAliases::const_iterator i = _fileLocationAliases.cbegin(), e = _fileLocationAliases.cend(); i != e; ++i) {
			// alias + location
			size += sizeof(quint64) * 2 + sizeof(quint64) * 2;
		}

		EncryptedDescriptor data(size);
		auto legacyTypeField = 0;
		for (FileLocations::const_iterator i = _fileLocations.cbegin(); i != _fileLocations.cend(); ++i) {
			data.stream << quint64(i.key().first) << quint64(i.key().second) << quint32(legacyTypeField) << i.value().name();
			if (AppVersion > 9013) {
				data.stream << i.value().bookmark();
			}
			data.stream << i.value().modified << quint32(i.value().size);
		}

		data.stream << quint64(0) << quint64(0) << quint32(0) << QString();
		if (AppVersion > 9013) {
			data.stream << QByteArray();
		}
		data.stream << QDateTime::currentDateTime() << quint32(0);

		data.stream << quint32(_fileLocationAliases.size());
		for (FileLocationAliases::const_iterator i = _fileLocationAliases.cbegin(), e = _fileLocationAliases.cend(); i != e; ++i) {
			data.stream << quint64(i.key().first) << quint64(i.key().second) << quint64(i.value().first) << quint64(i.value().second);
		}

		FileWriteDescriptor file(_locationsKey);
		file.writeEncrypted(data);
	}
}

void _readLocations() {
	FileReadDescriptor locations;
	if (!readEncryptedFile(locations, _locationsKey)) {
		clearKey(_locationsKey);
		_locationsKey = 0;
		_writeMap();
		return;
	}

	bool endMarkFound = false;
	while (!locations.stream.atEnd()) {
		quint64 first, second;
		QByteArray bookmark;
		FileLocation loc;
		quint32 legacyTypeField = 0;
		locations.stream >> first >> second >> legacyTypeField >> loc.fname;
		if (locations.version > 9013) {
			locations.stream >> bookmark;
		}
		locations.stream >> loc.modified >> loc.size;
		loc.setBookmark(bookmark);

		if (!first && !second && !legacyTypeField && loc.fname.isEmpty() && !loc.size) { // end mark
			endMarkFound = true;
			break;
		}

		MediaKey key(first, second);

		_fileLocations.insert(key, loc);
		_fileLocationPairs.insert(loc.fname, FileLocationPair(key, loc));
	}

	if (endMarkFound) {
		quint32 cnt;
		locations.stream >> cnt;
		for (quint32 i = 0; i < cnt; ++i) {
			quint64 kfirst, ksecond, vfirst, vsecond;
			locations.stream >> kfirst >> ksecond >> vfirst >> vsecond;
			_fileLocationAliases.insert(MediaKey(kfirst, ksecond), MediaKey(vfirst, vsecond));
		}

		if (!locations.stream.atEnd()) {
			quint32 webLocationsCount;
			locations.stream >> webLocationsCount;
			for (quint32 i = 0; i < webLocationsCount; ++i) {
				QString url;
				quint64 key;
				qint32 size;
				locations.stream >> url >> key >> size;
				clearKey(key, FileOption::User);
			}
		}
	}
}

void _writeReportSpamStatuses() {
	if (!_working()) return;

	if (cReportSpamStatuses().isEmpty()) {
		if (_reportSpamStatusesKey) {
			clearKey(_reportSpamStatusesKey);
			_reportSpamStatusesKey = 0;
			_mapChanged = true;
			_writeMap();
		}
	} else {
		if (!_reportSpamStatusesKey) {
			_reportSpamStatusesKey = genKey();
			_mapChanged = true;
			_writeMap(WriteMapWhen::Fast);
		}
		const ReportSpamStatuses &statuses(cReportSpamStatuses());

		quint32 size = sizeof(qint32);
		for (ReportSpamStatuses::const_iterator i = statuses.cbegin(), e = statuses.cend(); i != e; ++i) {
			// peer + status
			size += sizeof(quint64) + sizeof(qint32);
		}

		EncryptedDescriptor data(size);
		data.stream << qint32(statuses.size());
		for (ReportSpamStatuses::const_iterator i = statuses.cbegin(), e = statuses.cend(); i != e; ++i) {
			data.stream << quint64(i.key()) << qint32(i.value());
		}

		FileWriteDescriptor file(_reportSpamStatusesKey);
		file.writeEncrypted(data);
	}
}

void _readReportSpamStatuses() {
	FileReadDescriptor statuses;
	if (!readEncryptedFile(statuses, _reportSpamStatusesKey)) {
		clearKey(_reportSpamStatusesKey);
		_reportSpamStatusesKey = 0;
		_writeMap();
		return;
	}

	ReportSpamStatuses &map(cRefReportSpamStatuses());
	map.clear();

	qint32 size = 0;
	statuses.stream >> size;
	for (int32 i = 0; i < size; ++i) {
		quint64 peer = 0;
		qint32 status = 0;
		statuses.stream >> peer >> status;
		map.insert(peer, DBIPeerReportSpamStatus(status));
	}
}

struct ReadSettingsContext {
	int legacyLanguageId = Lang::kLegacyLanguageNone;
	QString legacyLanguageFile;
	MTP::DcOptions dcOptions;
};

void applyReadContext(ReadSettingsContext &&context) {
	Messenger::Instance().dcOptions()->addFromOther(std::move(context.dcOptions));
	if (context.legacyLanguageId != Lang::kLegacyLanguageNone) {
		Lang::Current().fillFromLegacy(context.legacyLanguageId, context.legacyLanguageFile);
		writeLangPack();
	}
}

bool _readSetting(quint32 blockId, QDataStream &stream, int version, ReadSettingsContext &context) {
	switch (blockId) {
	case dbiDcOptionOldOld: {
		quint32 dcId, port;
		QString host, ip;
		stream >> dcId >> host >> ip >> port;
		if (!_checkStreamStatus(stream)) return false;

		context.dcOptions.constructAddOne(
			dcId,
			0,
			ip.toStdString(),
			port,
			{});
	} break;

	case dbiDcOptionOld: {
		quint32 dcIdWithShift, port;
		qint32 flags;
		QString ip;
		stream >> dcIdWithShift >> flags >> ip >> port;
		if (!_checkStreamStatus(stream)) return false;

		context.dcOptions.constructAddOne(
			dcIdWithShift,
			MTPDdcOption::Flags::from_raw(flags),
			ip.toStdString(),
			port,
			{});
	} break;

	case dbiDcOptions: {
		auto serialized = QByteArray();
		stream >> serialized;
		if (!_checkStreamStatus(stream)) return false;

		context.dcOptions.constructFromSerialized(serialized);
	} break;

	case dbiChatSizeMax: {
		qint32 maxSize;
		stream >> maxSize;
		if (!_checkStreamStatus(stream)) return false;

		Global::SetChatSizeMax(maxSize);
	} break;

	case dbiSavedGifsLimit: {
		qint32 limit;
		stream >> limit;
		if (!_checkStreamStatus(stream)) return false;

		Global::SetSavedGifsLimit(limit);
	} break;

	case dbiStickersRecentLimit: {
		qint32 limit;
		stream >> limit;
		if (!_checkStreamStatus(stream)) return false;

		Global::SetStickersRecentLimit(limit);
	} break;

	case dbiStickersFavedLimit: {
		qint32 limit;
		stream >> limit;
		if (!_checkStreamStatus(stream)) return false;

		Global::SetStickersFavedLimit(limit);
	} break;

	case dbiMegagroupSizeMax: {
		qint32 maxSize;
		stream >> maxSize;
		if (!_checkStreamStatus(stream)) return false;

		Global::SetMegagroupSizeMax(maxSize);
	} break;

	case dbiUser: {
		quint32 dcId;
		qint32 userId;
		stream >> userId >> dcId;
		if (!_checkStreamStatus(stream)) return false;

		DEBUG_LOG(("MTP Info: user found, dc %1, uid %2").arg(dcId).arg(userId));
		Messenger::Instance().setMtpMainDcId(dcId);
		Messenger::Instance().setAuthSessionUserId(userId);
	} break;

	case dbiKey: {
		qint32 dcId;
		stream >> dcId;
		auto key = Serialize::read<MTP::AuthKey::Data>(stream);
		if (!_checkStreamStatus(stream)) return false;

		Messenger::Instance().setMtpKey(dcId, key);
	} break;

	case dbiMtpAuthorization: {
		auto serialized = QByteArray();
		stream >> serialized;
		if (!_checkStreamStatus(stream)) return false;

		Messenger::Instance().setMtpAuthorization(serialized);
	} break;

	case dbiAutoStart: {
		qint32 v;
		stream >> v;
		if (!_checkStreamStatus(stream)) return false;

		cSetAutoStart(v == 1);
	} break;

	case dbiStartMinimized: {
		qint32 v;
		stream >> v;
		if (!_checkStreamStatus(stream)) return false;

		cSetStartMinimized(v == 1);
	} break;

	case dbiSendToMenu: {
		qint32 v;
		stream >> v;
		if (!_checkStreamStatus(stream)) return false;

		cSetSendToMenu(v == 1);
	} break;

	case dbiUseExternalVideoPlayer: {
		qint32 v;
		stream >> v;
		if (!_checkStreamStatus(stream)) return false;

		cSetUseExternalVideoPlayer(v == 1);
	} break;

	case dbiCacheSettings: {
		qint64 size;
		qint32 time;
		stream >> size >> time;
		if (!_checkStreamStatus(stream)
			|| size <= Database::Settings().maxDataSize
			|| time < 0) {
			return false;
		}

		_cacheTotalSizeLimit = size;
		_cacheTotalTimeLimit = time;
	} break;

	case dbiAnimationsDisabled: {
		qint32 disabled;
		stream >> disabled;
		if (!_checkStreamStatus(stream)) {
			return false;
		}

		anim::SetDisabled(disabled == 1);
	} break;

	case dbiSoundNotify: {
		qint32 v;
		stream >> v;
		if (!_checkStreamStatus(stream)) return false;

		Global::SetSoundNotify(v == 1);
	} break;

	case dbiAutoDownload: {
		qint32 photo, audio, gif;
		stream >> photo >> audio >> gif;
		if (!_checkStreamStatus(stream)) return false;

		cSetAutoDownloadPhoto(photo);
		cSetAutoDownloadAudio(audio);
		cSetAutoDownloadGif(gif);
	} break;

	case dbiAutoPlay: {
		qint32 gif;
		stream >> gif;
		if (!_checkStreamStatus(stream)) return false;

		cSetAutoPlayGif(gif == 1);
	} break;

	case dbiDialogsMode: {
		qint32 enabled, modeInt;
		stream >> enabled >> modeInt;
		if (!_checkStreamStatus(stream)) return false;

		Global::SetDialogsModeEnabled(enabled == 1);
		auto mode = Dialogs::Mode::All;
		if (enabled) {
			mode = static_cast<Dialogs::Mode>(modeInt);
			if (mode != Dialogs::Mode::All && mode != Dialogs::Mode::Important) {
				mode = Dialogs::Mode::All;
			}
		}
		Global::SetDialogsMode(mode);
	} break;

	case dbiModerateMode: {
		qint32 enabled;
		stream >> enabled;
		if (!_checkStreamStatus(stream)) return false;

		Global::SetModerateModeEnabled(enabled == 1);
	} break;

	case dbiIncludeMutedOld: {
		qint32 v;
		stream >> v;
		if (!_checkStreamStatus(stream)) return false;

		GetStoredAuthSessionCache().setIncludeMutedCounter(v == 1);
	} break;

	case dbiShowingSavedGifsOld: {
		qint32 v;
		stream >> v;
		if (!_checkStreamStatus(stream)) return false;
	} break;

	case dbiDesktopNotify: {
		qint32 v;
		stream >> v;
		if (!_checkStreamStatus(stream)) return false;

		Global::SetDesktopNotify(v == 1);
		if (App::wnd()) App::wnd()->updateTrayMenu();
	} break;

	case dbiWindowsNotificationsOld: {
		qint32 v;
		stream >> v;
		if (!_checkStreamStatus(stream)) return false;
	} break;

	case dbiNativeNotifications: {
		qint32 v;
		stream >> v;
		if (!_checkStreamStatus(stream)) return false;

		Global::SetNativeNotifications(v == 1);
	} break;

	case dbiNotificationsCount: {
		qint32 v;
		stream >> v;
		if (!_checkStreamStatus(stream)) return false;

		Global::SetNotificationsCount((v > 0 ? v : 3));
	} break;

	case dbiNotificationsCorner: {
		qint32 v;
		stream >> v;
		if (!_checkStreamStatus(stream)) return false;

		Global::SetNotificationsCorner(static_cast<Notify::ScreenCorner>((v >= 0 && v < 4) ? v : 2));
	} break;

	case dbiDialogsWidthRatioOld: {
		qint32 v;
		stream >> v;
		if (!_checkStreamStatus(stream)) return false;

		GetStoredAuthSessionCache().setDialogsWidthRatio(v / 1000000.);
	} break;

	case dbiLastSeenWarningSeenOld: {
		qint32 v;
		stream >> v;
		if (!_checkStreamStatus(stream)) return false;

		GetStoredAuthSessionCache().setLastSeenWarningSeen(v == 1);
	} break;

	case dbiAuthSessionSettings: {
		QByteArray v;
		stream >> v;
		if (!_checkStreamStatus(stream)) return false;

		GetStoredAuthSessionCache().constructFromSerialized(v);
	} break;

	case dbiWorkMode: {
		qint32 v;
		stream >> v;
		if (!_checkStreamStatus(stream)) return false;

		auto newMode = [v] {
			switch (v) {
			case dbiwmTrayOnly: return dbiwmTrayOnly;
			case dbiwmWindowOnly: return dbiwmWindowOnly;
			};
			return dbiwmWindowAndTray;
		};
		Global::RefWorkMode().set(newMode());
	} break;

	case dbiTxtDomainString: {
		QString v;
		stream >> v;
		if (!_checkStreamStatus(stream)) return false;

		Global::SetTxtDomainString(v);
	} break;

	case dbiConnectionTypeOld: {
		qint32 v;
		stream >> v;
		if (!_checkStreamStatus(stream)) return false;

		ProxyData proxy;
		switch (v) {
		case dbictHttpProxy:
		case dbictTcpProxy: {
			qint32 port;
			stream >> proxy.host >> port >> proxy.user >> proxy.password;
			if (!_checkStreamStatus(stream)) return false;

			proxy.port = uint32(port);
			proxy.type = (v == dbictTcpProxy)
				? ProxyData::Type::Socks5
				: ProxyData::Type::Http;
		} break;
		};
		Global::SetSelectedProxy(proxy ? proxy : ProxyData());
		Global::SetProxySettings(proxy
			? ProxyData::Settings::Enabled
			: ProxyData::Settings::System);
		if (proxy) {
			Global::SetProxiesList({ 1, proxy });
		} else {
			Global::SetProxiesList({});
		}
		Sandbox::refreshGlobalProxy();
	} break;

	case dbiConnectionType: {
		qint32 connectionType;
		stream >> connectionType;
		if (!_checkStreamStatus(stream)) {
			return false;
		}

		const auto readProxy = [&] {
			qint32 proxyType, port;
			ProxyData proxy;
			stream >> proxyType >> proxy.host >> port >> proxy.user >> proxy.password;
			proxy.port = port;
			proxy.type = (proxyType == dbictTcpProxy)
				? ProxyData::Type::Socks5
				: (proxyType == dbictHttpProxy)
				? ProxyData::Type::Http
				: (proxyType == kProxyTypeShift + int(ProxyData::Type::Socks5))
				? ProxyData::Type::Socks5
				: (proxyType == kProxyTypeShift + int(ProxyData::Type::Http))
				? ProxyData::Type::Http
				: (proxyType == kProxyTypeShift + int(ProxyData::Type::Mtproto))
				? ProxyData::Type::Mtproto
				: ProxyData::Type::None;
			return proxy;
		};
		if (connectionType == dbictProxiesListOld
			|| connectionType == dbictProxiesList) {
			qint32 count = 0, index = 0;
			stream >> count >> index;
			qint32 settings = 0, calls = 0;
			if (connectionType == dbictProxiesList) {
				stream >> settings >> calls;
			} else if (std::abs(index) > count) {
				calls = 1;
				index -= (index > 0 ? count : -count);
			}

			auto list = std::vector<ProxyData>();
			for (auto i = 0; i < count; ++i) {
				const auto proxy = readProxy();
				if (proxy) {
					list.push_back(proxy);
				} else if (index < -list.size()) {
					++index;
				} else if (index > list.size()) {
					--index;
				}
			}
			if (!_checkStreamStatus(stream)) {
				return false;
			}
			Global::SetProxiesList(list);
			if (connectionType == dbictProxiesListOld) {
				settings = static_cast<qint32>(
					(index > 0 && index <= list.size()
						? ProxyData::Settings::Enabled
						: ProxyData::Settings::System));
				index = std::abs(index);
			}
			if (index > 0 && index <= list.size()) {
				Global::SetSelectedProxy(list[index - 1]);
			} else {
				Global::SetSelectedProxy(ProxyData());
			}

			const auto unchecked = static_cast<ProxyData::Settings>(settings);
			switch (unchecked) {
			case ProxyData::Settings::Disabled:
			case ProxyData::Settings::System:
			case ProxyData::Settings::Enabled:
				Global::SetProxySettings(unchecked);
				break;
			default:
				Global::SetProxySettings(ProxyData::Settings::System);
				break;
			}
			Global::SetUseProxyForCalls(calls == 1);
		} else {
			const auto proxy = readProxy();
			if (!_checkStreamStatus(stream)) {
				return false;
			}
			if (proxy) {
				Global::SetProxiesList({ 1, proxy });
				Global::SetSelectedProxy(proxy);
				if (connectionType == dbictTcpProxy
					|| connectionType == dbictHttpProxy) {
					Global::SetProxySettings(ProxyData::Settings::Enabled);
				} else {
					Global::SetProxySettings(ProxyData::Settings::System);
				}
			} else {
				Global::SetProxiesList({});
				Global::SetSelectedProxy(ProxyData());
				Global::SetProxySettings(ProxyData::Settings::System);
			}
		}
		Sandbox::refreshGlobalProxy();
	} break;

	case dbiThemeKeyOld: {
		quint64 key = 0;
		stream >> key;
		if (!_checkStreamStatus(stream)) return false;

		_themeKeyLegacy = key;
	} break;

	case dbiThemeKey: {
		quint64 keyDay = 0, keyNight = 0;
		quint32 nightMode = 0;
		stream >> keyDay >> keyNight >> nightMode;
		if (!_checkStreamStatus(stream)) return false;

		_themeKeyDay = keyDay;
		_themeKeyNight = keyNight;
		Window::Theme::SetNightModeValue(nightMode == 1);
	} break;

	case dbiLangPackKey: {
		quint64 langPackKey = 0;
		stream >> langPackKey;
		if (!_checkStreamStatus(stream)) return false;

		_langPackKey = langPackKey;
	} break;

	case dbiLanguagesKey: {
		quint64 languagesKey = 0;
		stream >> languagesKey;
		if (!_checkStreamStatus(stream)) return false;

		_languagesKey = languagesKey;
	} break;

	case dbiTryIPv6: {
		qint32 v;
		stream >> v;
		if (!_checkStreamStatus(stream)) return false;

		Global::SetTryIPv6(v == 1);
	} break;

	case dbiSeenTrayTooltip: {
		qint32 v;
		stream >> v;
		if (!_checkStreamStatus(stream)) return false;

		cSetSeenTrayTooltip(v == 1);
	} break;

	case dbiAutoUpdate: {
		qint32 v;
		stream >> v;
		if (!_checkStreamStatus(stream)) return false;

		cSetAutoUpdate(v == 1);
		if (!Core::UpdaterDisabled() && !cAutoUpdate()) {
			Core::UpdateChecker().stop();
		}
	} break;

	case dbiLastUpdateCheck: {
		qint32 v;
		stream >> v;
		if (!_checkStreamStatus(stream)) return false;

		cSetLastUpdateCheck(v);
	} break;

	case dbiScaleOld: {
		qint32 v;
		stream >> v;
		if (!_checkStreamStatus(stream)) return false;

		SetScaleChecked([&] {
			constexpr auto kAuto = 0;
			constexpr auto kOne = 1;
			constexpr auto kOneAndQuarter = 2;
			constexpr auto kOneAndHalf = 3;
			constexpr auto kTwo = 4;
			switch (v) {
			case kAuto: return kInterfaceScaleAuto;
			case kOne: return 100;
			case kOneAndQuarter: return 125;
			case kOneAndHalf: return 150;
			case kTwo: return 200;
			}
			return cConfigScale();
		}());
	} break;

	case dbiScalePercent: {
		qint32 v;
		stream >> v;
		if (!_checkStreamStatus(stream)) return false;

		SetScaleChecked(v);
	} break;

	case dbiLangOld: {
		qint32 v;
		stream >> v;
		if (!_checkStreamStatus(stream)) return false;

		context.legacyLanguageId = v;
	} break;

	case dbiLangFileOld: {
		QString v;
		stream >> v;
		if (!_checkStreamStatus(stream)) return false;

		context.legacyLanguageFile = v;
	} break;

	case dbiWindowPosition: {
		auto position = TWindowPos();
		stream >> position.x >> position.y >> position.w >> position.h;
		stream >> position.moncrc >> position.maximized;
		if (!_checkStreamStatus(stream)) return false;

		DEBUG_LOG(("Window Pos: Read from storage %1, %2, %3, %4 (maximized %5)").arg(position.x).arg(position.y).arg(position.w).arg(position.h).arg(Logs::b(position.maximized)));
		cSetWindowPos(position);
	} break;

	case dbiLoggedPhoneNumber: {
		QString v;
		stream >> v;
		if (!_checkStreamStatus(stream)) return false;

		cSetLoggedPhoneNumber(v);
	} break;

	case dbiMutePeer: { // deprecated
		quint64 peerId;
		stream >> peerId;
		if (!_checkStreamStatus(stream)) return false;
	} break;

	case dbiMutedPeers: { // deprecated
		quint32 count;
		stream >> count;
		if (!_checkStreamStatus(stream)) return false;

		for (uint32 i = 0; i < count; ++i) {
			quint64 peerId;
			stream >> peerId;
		}
		if (!_checkStreamStatus(stream)) return false;
	} break;

	case dbiSendKeyOld: {
		qint32 v;
		stream >> v;
		if (!_checkStreamStatus(stream)) return false;

		using SendSettings = Ui::InputSubmitSettings;
		const auto unchecked = static_cast<SendSettings>(v);

		if (unchecked != SendSettings::Enter
			&& unchecked != SendSettings::CtrlEnter) {
			return false;
		}
		GetStoredAuthSessionCache().setSendSubmitWay(unchecked);
	} break;

	case dbiCatsAndDogs: { // deprecated
		qint32 v;
		stream >> v;
		if (!_checkStreamStatus(stream)) return false;
	} break;

	case dbiTileBackgroundOld: {
		qint32 v;
		stream >> v;
		if (!_checkStreamStatus(stream)) return false;

		bool tile = (version < 8005 && !_backgroundKeyDay)
			? false
			: (v == 1);
		if (Window::Theme::IsNightMode()) {
			Window::Theme::Background()->setTileNightValue(tile);
		} else {
			Window::Theme::Background()->setTileDayValue(tile);
		}
	} break;

	case dbiTileBackground: {
		qint32 tileDay, tileNight;
		stream >> tileDay >> tileNight;
		if (!_checkStreamStatus(stream)) return false;

		Window::Theme::Background()->setTileDayValue(tileDay == 1);
		Window::Theme::Background()->setTileNightValue(tileNight == 1);
	} break;

	case dbiAdaptiveForWide: {
		qint32 v;
		stream >> v;
		if (!_checkStreamStatus(stream)) return false;

		Global::SetAdaptiveForWide(v == 1);
	} break;

	case dbiAutoLock: {
		qint32 v;
		stream >> v;
		if (!_checkStreamStatus(stream)) return false;

		Global::SetAutoLock(v);
		Global::RefLocalPasscodeChanged().notify();
	} break;

	case dbiReplaceEmoji: {
		qint32 v;
		stream >> v;
		if (!_checkStreamStatus(stream)) return false;

		Global::SetReplaceEmoji(v == 1);
	} break;

	case dbiSuggestEmoji: {
		qint32 v;
		stream >> v;
		if (!_checkStreamStatus(stream)) return false;

		Global::SetSuggestEmoji(v == 1);
	} break;

	case dbiSuggestStickersByEmoji: {
		qint32 v;
		stream >> v;
		if (!_checkStreamStatus(stream)) return false;

		Global::SetSuggestStickersByEmoji(v == 1);
	} break;

	case dbiDefaultAttach: {
		qint32 v;
		stream >> v;
		if (!_checkStreamStatus(stream)) return false;
	} break;

	case dbiNotifyView: {
		qint32 v;
		stream >> v;
		if (!_checkStreamStatus(stream)) return false;

		switch (v) {
		case dbinvShowNothing: Global::SetNotifyView(dbinvShowNothing); break;
		case dbinvShowName: Global::SetNotifyView(dbinvShowName); break;
		default: Global::SetNotifyView(dbinvShowPreview); break;
		}
	} break;

	case dbiAskDownloadPath: {
		qint32 v;
		stream >> v;
		if (!_checkStreamStatus(stream)) return false;

		Global::SetAskDownloadPath(v == 1);
	} break;

	case dbiDownloadPathOld: {
		QString v;
		stream >> v;
		if (!_checkStreamStatus(stream)) return false;
#ifndef OS_WIN_STORE
		if (!v.isEmpty() && v != qstr("tmp") && !v.endsWith('/')) v += '/';
		Global::SetDownloadPath(v);
		Global::SetDownloadPathBookmark(QByteArray());
		Global::RefDownloadPathChanged().notify();
#endif // OS_WIN_STORE
	} break;

	case dbiDownloadPath: {
		QString v;
		QByteArray bookmark;
		stream >> v >> bookmark;
		if (!_checkStreamStatus(stream)) return false;
#ifndef OS_WIN_STORE
		if (!v.isEmpty() && v != qstr("tmp") && !v.endsWith('/')) v += '/';
		Global::SetDownloadPath(v);
		Global::SetDownloadPathBookmark(bookmark);
		psDownloadPathEnableAccess();
		Global::RefDownloadPathChanged().notify();
#endif // OS_WIN_STORE
	} break;

	case dbiCompressPastedImage: {
		qint32 v;
		stream >> v;
		if (!_checkStreamStatus(stream)) return false;

		GetStoredAuthSessionCache().setSendFilesWay((v == 1)
			? SendFilesWay::Album
			: SendFilesWay::Files);
	} break;

	case dbiEmojiTabOld: {
		qint32 v;
		stream >> v;
		if (!_checkStreamStatus(stream)) return false;

		// deprecated
	} break;

	case dbiRecentEmojiOldOld: {
		RecentEmojiPreloadOldOld v;
		stream >> v;
		if (!_checkStreamStatus(stream)) return false;

		if (!v.isEmpty()) {
			RecentEmojiPreload p;
			p.reserve(v.size());
			for (auto &item : v) {
				auto oldKey = uint64(item.first);
				switch (oldKey) {
				case 0xD83CDDEFLLU: oldKey = 0xD83CDDEFD83CDDF5LLU; break;
				case 0xD83CDDF0LLU: oldKey = 0xD83CDDF0D83CDDF7LLU; break;
				case 0xD83CDDE9LLU: oldKey = 0xD83CDDE9D83CDDEALLU; break;
				case 0xD83CDDE8LLU: oldKey = 0xD83CDDE8D83CDDF3LLU; break;
				case 0xD83CDDFALLU: oldKey = 0xD83CDDFAD83CDDF8LLU; break;
				case 0xD83CDDEBLLU: oldKey = 0xD83CDDEBD83CDDF7LLU; break;
				case 0xD83CDDEALLU: oldKey = 0xD83CDDEAD83CDDF8LLU; break;
				case 0xD83CDDEELLU: oldKey = 0xD83CDDEED83CDDF9LLU; break;
				case 0xD83CDDF7LLU: oldKey = 0xD83CDDF7D83CDDFALLU; break;
				case 0xD83CDDECLLU: oldKey = 0xD83CDDECD83CDDE7LLU; break;
				}
				auto id = Ui::Emoji::IdFromOldKey(oldKey);
				if (!id.isEmpty()) {
					p.push_back(qMakePair(id, item.second));
				}
			}
			cSetRecentEmojiPreload(p);
		}
	} break;

	case dbiRecentEmojiOld: {
		RecentEmojiPreloadOld v;
		stream >> v;
		if (!_checkStreamStatus(stream)) return false;

		if (!v.isEmpty()) {
			RecentEmojiPreload p;
			p.reserve(v.size());
			for (auto &item : v) {
				auto id = Ui::Emoji::IdFromOldKey(item.first);
				if (!id.isEmpty()) {
					p.push_back(qMakePair(id, item.second));
				}
			}
			cSetRecentEmojiPreload(p);
		}
	} break;

	case dbiRecentEmoji: {
		RecentEmojiPreload v;
		stream >> v;
		if (!_checkStreamStatus(stream)) return false;

		cSetRecentEmojiPreload(v);
	} break;

	case dbiRecentStickers: {
		RecentStickerPreload v;
		stream >> v;
		if (!_checkStreamStatus(stream)) return false;

		cSetRecentStickersPreload(v);
	} break;

	case dbiEmojiVariantsOld: {
		EmojiColorVariantsOld v;
		stream >> v;
		if (!_checkStreamStatus(stream)) return false;

		EmojiColorVariants variants;
		for (auto i = v.cbegin(), e = v.cend(); i != e; ++i) {
			auto id = Ui::Emoji::IdFromOldKey(static_cast<uint64>(i.key()));
			if (!id.isEmpty()) {
				auto index = Ui::Emoji::ColorIndexFromOldKey(i.value());
				if (index >= 0) {
					variants.insert(id, index);
				}
			}
		}
		cSetEmojiVariants(variants);
	} break;

	case dbiEmojiVariants: {
		EmojiColorVariants v;
		stream >> v;
		if (!_checkStreamStatus(stream)) return false;

		cSetEmojiVariants(v);
	} break;

	case dbiHiddenPinnedMessages: {
		Global::HiddenPinnedMessagesMap v;
		stream >> v;
		if (!_checkStreamStatus(stream)) return false;

		Global::SetHiddenPinnedMessages(v);
	} break;

	case dbiDialogLastPath: {
		QString path;
		stream >> path;
		if (!_checkStreamStatus(stream)) return false;

		cSetDialogLastPath(path);
	} break;

	case dbiSongVolume: {
		qint32 v;
		stream >> v;
		if (!_checkStreamStatus(stream)) return false;

		Global::SetSongVolume(snap(v / 1e6, 0., 1.));
	} break;

	case dbiVideoVolume: {
		qint32 v;
		stream >> v;
		if (!_checkStreamStatus(stream)) return false;

		Global::SetVideoVolume(snap(v / 1e6, 0., 1.));
	} break;

	case dbiPlaybackSpeed: {
		qint32 v;
		stream >> v;
		if (!_checkStreamStatus(stream)) return false;

		Global::SetVoiceMsgPlaybackDoubled(v == 2);
	} break;

	default:
	LOG(("App Error: unknown blockId in _readSetting: %1").arg(blockId));
	return false;
	}

	return true;
}

bool _readOldSettings(bool remove, ReadSettingsContext &context) {
	bool result = false;
	QFile file(cWorkingDir() + qsl("tdata/config"));
	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;
			}

			createLocalKey(QByteArray(), &salt, &OldKey);

			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;
	QFile file(cWorkingDir() + cDataFile() + (cTestMode() ? qsl("_test") : QString()) + qsl("_config"));
	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;
	QFile file(cWorkingDir() + cDataFile() + (cTestMode() ? qsl("_test") : QString()));
	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;
}

void _writeUserSettings() {
	if (_readingUserSettings) {
		LOG(("App Error: attempt to write settings while reading them!"));
		return;
	}
	LOG(("App Info: writing encrypted user settings..."));

	if (!_userSettingsKey) {
		_userSettingsKey = genKey();
		_mapChanged = true;
		_writeMap(WriteMapWhen::Fast);
	}

	auto recentEmojiPreloadData = cRecentEmojiPreload();
	if (recentEmojiPreloadData.isEmpty()) {
		recentEmojiPreloadData.reserve(Ui::Emoji::GetRecent().size());
		for (auto &item : Ui::Emoji::GetRecent()) {
			recentEmojiPreloadData.push_back(qMakePair(item.first->id(), item.second));
		}
	}
	auto userDataInstance = StoredAuthSessionCache
		? StoredAuthSessionCache.get()
		: Messenger::Instance().getAuthSessionSettings();
	auto userData = userDataInstance
		? userDataInstance->serialize()
		: QByteArray();

	uint32 size = 23 * (sizeof(quint32) + sizeof(qint32));
	size += sizeof(quint32) + Serialize::stringSize(Global::AskDownloadPath() ? QString() : Global::DownloadPath()) + Serialize::bytearraySize(Global::AskDownloadPath() ? QByteArray() : Global::DownloadPathBookmark());

	size += sizeof(quint32) + sizeof(qint32);
	for (auto &item : recentEmojiPreloadData) {
		size += Serialize::stringSize(item.first) + sizeof(item.second);
	}

	size += sizeof(quint32) + sizeof(qint32) + cEmojiVariants().size() * (sizeof(uint32) + sizeof(uint64));
	size += sizeof(quint32) + sizeof(qint32) + (cRecentStickersPreload().isEmpty() ? Stickers::GetRecentPack().size() : cRecentStickersPreload().size()) * (sizeof(uint64) + sizeof(ushort));
	size += sizeof(quint32) + Serialize::stringSize(cDialogLastPath());
	size += sizeof(quint32) + 3 * sizeof(qint32);
	size += sizeof(quint32) + 2 * sizeof(qint32);
	size += sizeof(quint32) + 2 * sizeof(qint32);
	size += sizeof(quint32) + sizeof(qint64) + sizeof(qint32);
	if (!Global::HiddenPinnedMessages().isEmpty()) {
		size += sizeof(quint32) + sizeof(qint32) + Global::HiddenPinnedMessages().size() * (sizeof(PeerId) + sizeof(MsgId));
	}
	if (!userData.isEmpty()) {
		size += sizeof(quint32) + Serialize::bytearraySize(userData);
	}

	EncryptedDescriptor data(size);
	data.stream
		<< quint32(dbiTileBackground)
		<< qint32(Window::Theme::Background()->tileDay() ? 1 : 0)
		<< qint32(Window::Theme::Background()->tileNight() ? 1 : 0);
	data.stream << quint32(dbiAdaptiveForWide) << qint32(Global::AdaptiveForWide() ? 1 : 0);
	data.stream << quint32(dbiAutoLock) << qint32(Global::AutoLock());
	data.stream << quint32(dbiReplaceEmoji) << qint32(Global::ReplaceEmoji() ? 1 : 0);
	data.stream << quint32(dbiSuggestEmoji) << qint32(Global::SuggestEmoji() ? 1 : 0);
	data.stream << quint32(dbiSuggestStickersByEmoji) << qint32(Global::SuggestStickersByEmoji() ? 1 : 0);
	data.stream << quint32(dbiSoundNotify) << qint32(Global::SoundNotify());
	data.stream << quint32(dbiDesktopNotify) << qint32(Global::DesktopNotify());
	data.stream << quint32(dbiNotifyView) << qint32(Global::NotifyView());
	data.stream << quint32(dbiNativeNotifications) << qint32(Global::NativeNotifications());
	data.stream << quint32(dbiNotificationsCount) << qint32(Global::NotificationsCount());
	data.stream << quint32(dbiNotificationsCorner) << qint32(Global::NotificationsCorner());
	data.stream << quint32(dbiAskDownloadPath) << qint32(Global::AskDownloadPath());
	data.stream << quint32(dbiDownloadPath) << (Global::AskDownloadPath() ? QString() : Global::DownloadPath()) << (Global::AskDownloadPath() ? QByteArray() : Global::DownloadPathBookmark());
	data.stream << quint32(dbiDialogLastPath) << cDialogLastPath();
	data.stream << quint32(dbiSongVolume) << qint32(qRound(Global::SongVolume() * 1e6));
	data.stream << quint32(dbiVideoVolume) << qint32(qRound(Global::VideoVolume() * 1e6));
	data.stream << quint32(dbiAutoDownload) << qint32(cAutoDownloadPhoto()) << qint32(cAutoDownloadAudio()) << qint32(cAutoDownloadGif());
	data.stream << quint32(dbiDialogsMode) << qint32(Global::DialogsModeEnabled() ? 1 : 0) << static_cast<qint32>(Global::DialogsMode());
	data.stream << quint32(dbiModerateMode) << qint32(Global::ModerateModeEnabled() ? 1 : 0);
	data.stream << quint32(dbiAutoPlay) << qint32(cAutoPlayGif() ? 1 : 0);
	data.stream << quint32(dbiUseExternalVideoPlayer) << qint32(cUseExternalVideoPlayer());
	data.stream << quint32(dbiCacheSettings) << qint64(_cacheTotalSizeLimit) << qint32(_cacheTotalTimeLimit);
	if (!userData.isEmpty()) {
		data.stream << quint32(dbiAuthSessionSettings) << userData;
	}
	data.stream << quint32(dbiPlaybackSpeed) << qint32(Global::VoiceMsgPlaybackDoubled() ? 2 : 1);

	{
		data.stream << quint32(dbiRecentEmoji) << recentEmojiPreloadData;
	}
	data.stream << quint32(dbiEmojiVariants) << cEmojiVariants();
	{
		auto v = cRecentStickersPreload();
		if (v.isEmpty()) {
			v.reserve(Stickers::GetRecentPack().size());
			for_const (auto &pair, Stickers::GetRecentPack()) {
				v.push_back(qMakePair(pair.first->id, pair.second));
			}
		}
		data.stream << quint32(dbiRecentStickers) << v;
	}
	if (!Global::HiddenPinnedMessages().isEmpty()) {
		data.stream << quint32(dbiHiddenPinnedMessages) << Global::HiddenPinnedMessages();
	}

	FileWriteDescriptor file(_userSettingsKey);
	file.writeEncrypted(data);
}

void _readUserSettings() {
	ReadSettingsContext context;
	FileReadDescriptor userSettings;
	if (!readEncryptedFile(userSettings, _userSettingsKey)) {
		LOG(("App Info: could not read encrypted user settings..."));

		_readOldUserSettings(true, context);
		applyReadContext(std::move(context));

		return _writeUserSettings();
	}

	LOG(("App Info: reading encrypted user settings..."));
	_readingUserSettings = true;
	while (!userSettings.stream.atEnd()) {
		quint32 blockId;
		userSettings.stream >> blockId;
		if (!_checkStreamStatus(userSettings.stream)) {
			_readingUserSettings = false;
			return _writeUserSettings();
		}

		if (!_readSetting(blockId, userSettings.stream, userSettings.version, context)) {
			_readingUserSettings = false;
			return _writeUserSettings();
		}
	}
	_readingUserSettings = false;
	LOG(("App Info: encrypted user settings read."));

	applyReadContext(std::move(context));
}

void _writeMtpData() {
	FileWriteDescriptor mtp(toFilePart(_dataNameKey), FileOption::Safe);
	if (!LocalKey) {
		LOG(("App Error: localkey not created in _writeMtpData()"));
		return;
	}

	auto mtpAuthorizationSerialized = Messenger::Instance().serializeMtpAuthorization();

	quint32 size = sizeof(quint32) + Serialize::bytearraySize(mtpAuthorizationSerialized);

	EncryptedDescriptor data(size);
	data.stream << quint32(dbiMtpAuthorization) << mtpAuthorizationSerialized;
	mtp.writeEncrypted(data);
}

void _readMtpData() {
	ReadSettingsContext context;
	FileReadDescriptor mtp;
	if (!readEncryptedFile(mtp, toFilePart(_dataNameKey), FileOption::Safe)) {
		if (LocalKey) {
			_readOldMtpData(true, context);
			applyReadContext(std::move(context));

			_writeMtpData();
		}
		return;
	}

	LOG(("App Info: reading encrypted mtp data..."));
	while (!mtp.stream.atEnd()) {
		quint32 blockId;
		mtp.stream >> blockId;
		if (!_checkStreamStatus(mtp.stream)) {
			return _writeMtpData();
		}

		if (!_readSetting(blockId, mtp.stream, mtp.version, context)) {
			return _writeMtpData();
		}
	}
	applyReadContext(std::move(context));
}

ReadMapState _readMap(const QByteArray &pass) {
	auto ms = getms();
	QByteArray dataNameUtf8 = (cDataFile() + (cTestMode() ? qsl(":/test/") : QString())).toUtf8();
	FileKey dataNameHash[2];
	hashMd5(dataNameUtf8.constData(), dataNameUtf8.size(), dataNameHash);
	_dataNameKey = dataNameHash[0];
	_userBasePath = _basePath + toFilePart(_dataNameKey) + QChar('/');
	_userDbPath = _basePath
		+ "user_" + cDataFile()
		+ (cTestMode() ? "[test]" : "")
		+ '/';

	FileReadDescriptor mapData;
	if (!readFile(mapData, qsl("map"))) {
		return ReadMapFailed;
	}
	LOG(("App Info: reading map..."));

	QByteArray salt, keyEncrypted, mapEncrypted;
	mapData.stream >> salt >> keyEncrypted >> mapEncrypted;
	if (!_checkStreamStatus(mapData.stream)) {
		return ReadMapFailed;
	}

	if (salt.size() != LocalEncryptSaltSize) {
		LOG(("App Error: bad salt in map file, size: %1").arg(salt.size()));
		return ReadMapFailed;
	}
	createLocalKey(pass, &salt, &PassKey);

	EncryptedDescriptor keyData, map;
	if (!decryptLocal(keyData, keyEncrypted, PassKey)) {
		LOG(("App Info: could not decrypt pass-protected key from map file, maybe bad password..."));
		return ReadMapPassNeeded;
	}
	auto key = Serialize::read<MTP::AuthKey::Data>(keyData.stream);
	if (keyData.stream.status() != QDataStream::Ok || !keyData.stream.atEnd()) {
		LOG(("App Error: could not read pass-protected key from map file"));
		return ReadMapFailed;
	}
	LocalKey = std::make_shared<MTP::AuthKey>(key);

	_passKeyEncrypted = keyEncrypted;
	_passKeySalt = salt;

	if (!decryptLocal(map, mapEncrypted)) {
		LOG(("App Error: could not decrypt map."));
		return ReadMapFailed;
	}
	LOG(("App Info: reading encrypted map..."));

	QByteArray selfSerialized;
	DraftsMap draftsMap, draftCursorsMap;
	DraftsNotReadMap draftsNotReadMap;
	quint64 locationsKey = 0, reportSpamStatusesKey = 0, trustedBotsKey = 0;
	quint64 recentStickersKeyOld = 0;
	quint64 installedStickersKey = 0, featuredStickersKey = 0, recentStickersKey = 0, favedStickersKey = 0, archivedStickersKey = 0;
	quint64 savedGifsKey = 0;
	quint64 backgroundKeyDay = 0, backgroundKeyNight = 0;
	quint64 userSettingsKey = 0, recentHashtagsAndBotsKey = 0, savedPeersKey = 0, exportSettingsKey = 0;
	while (!map.stream.atEnd()) {
		quint32 keyType;
		map.stream >> keyType;
		switch (keyType) {
		case lskDraft: {
			quint32 count = 0;
			map.stream >> count;
			for (quint32 i = 0; i < count; ++i) {
				FileKey key;
				quint64 p;
				map.stream >> key >> p;
				draftsMap.insert(p, key);
				draftsNotReadMap.insert(p, true);
			}
		} break;
		case lskSelfSerialized: {
			map.stream >> selfSerialized;
		} break;
		case lskDraftPosition: {
			quint32 count = 0;
			map.stream >> count;
			for (quint32 i = 0; i < count; ++i) {
				FileKey key;
				quint64 p;
				map.stream >> key >> p;
				draftCursorsMap.insert(p, key);
			}
		} break;
		case lskImages:
		case lskStickerImages:
		case lskAudios: {
			quint32 count = 0;
			map.stream >> count;
			for (quint32 i = 0; i < count; ++i) {
				FileKey key;
				quint64 first, second;
				qint32 size;
				map.stream >> key >> first >> second >> size;
				// Just ignore the key, it will be removed as a leaked one.
			}
		} break;
		case lskLocations: {
			map.stream >> locationsKey;
		} break;
		case lskReportSpamStatuses: {
			map.stream >> reportSpamStatusesKey;
		} break;
		case lskTrustedBots: {
			map.stream >> trustedBotsKey;
		} break;
		case lskRecentStickersOld: {
			map.stream >> recentStickersKeyOld;
		} break;
		case lskBackgroundOld: {
			map.stream >> (Window::Theme::IsNightMode()
				? backgroundKeyNight
				: backgroundKeyDay);
		} break;
		case lskBackground: {
			map.stream >> backgroundKeyDay >> backgroundKeyNight;
		} break;
		case lskUserSettings: {
			map.stream >> userSettingsKey;
		} break;
		case lskRecentHashtagsAndBots: {
			map.stream >> recentHashtagsAndBotsKey;
		} break;
		case lskStickersOld: {
			map.stream >> installedStickersKey;
		} break;
		case lskStickersKeys: {
			map.stream >> installedStickersKey >> featuredStickersKey >> recentStickersKey >> archivedStickersKey;
		} break;
		case lskFavedStickers: {
			map.stream >> favedStickersKey;
		} break;
		case lskSavedGifsOld: {
			quint64 key;
			map.stream >> key;
		} break;
		case lskSavedGifs: {
			map.stream >> savedGifsKey;
		} break;
		case lskSavedPeers: {
			map.stream >> savedPeersKey;
		} break;
		case lskExportSettings: {
			map.stream >> exportSettingsKey;
		} break;
		default:
		LOG(("App Error: unknown key type in encrypted map: %1").arg(keyType));
		return ReadMapFailed;
		}
		if (!_checkStreamStatus(map.stream)) {
			return ReadMapFailed;
		}
	}

	_draftsMap = draftsMap;
	_draftCursorsMap = draftCursorsMap;
	_draftsNotReadMap = draftsNotReadMap;

	_locationsKey = locationsKey;
	_reportSpamStatusesKey = reportSpamStatusesKey;
	_trustedBotsKey = trustedBotsKey;
	_recentStickersKeyOld = recentStickersKeyOld;
	_installedStickersKey = installedStickersKey;
	_featuredStickersKey = featuredStickersKey;
	_recentStickersKey = recentStickersKey;
	_favedStickersKey = favedStickersKey;
	_archivedStickersKey = archivedStickersKey;
	_savedGifsKey = savedGifsKey;
	_savedPeersKey = savedPeersKey;
	_backgroundKeyDay = backgroundKeyDay;
	_backgroundKeyNight = backgroundKeyNight;
	_userSettingsKey = userSettingsKey;
	_recentHashtagsAndBotsKey = recentHashtagsAndBotsKey;
	_exportSettingsKey = exportSettingsKey;
	_oldMapVersion = mapData.version;
	if (_oldMapVersion < AppVersion) {
		_mapChanged = true;
		_writeMap();
	} else {
		_mapChanged = false;
	}

	if (_locationsKey) {
		_readLocations();
	}
	if (_reportSpamStatusesKey) {
		_readReportSpamStatuses();
	}

	_readUserSettings();
	_readMtpData();

	DEBUG_LOG(("selfSerialized set: %1").arg(selfSerialized.size()));
	Messenger::Instance().setAuthSessionFromStorage(
		std::move(StoredAuthSessionCache),
		std::move(selfSerialized),
		_oldMapVersion);

	LOG(("Map read time: %1").arg(getms() - ms));
	if (_oldSettingsVersion < AppVersion) {
		writeSettings();
	}
	return ReadMapDone;
}

void _writeMap(WriteMapWhen when) {
	if (when != WriteMapWhen::Now) {
		_manager->writeMap(when == WriteMapWhen::Fast);
		return;
	}
	_manager->writingMap();
	if (!_mapChanged) return;
	if (_userBasePath.isEmpty()) {
		LOG(("App Error: _userBasePath is empty in writeMap()"));
		return;
	}

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

	FileWriteDescriptor map(qsl("map"));
	if (_passKeySalt.isEmpty() || _passKeyEncrypted.isEmpty()) {
		QByteArray pass(kLocalKeySize, Qt::Uninitialized), salt(LocalEncryptSaltSize, Qt::Uninitialized);
		memset_rand(pass.data(), pass.size());
		memset_rand(salt.data(), salt.size());
		createLocalKey(pass, &salt, &LocalKey);

		_passKeySalt.resize(LocalEncryptSaltSize);
		memset_rand(_passKeySalt.data(), _passKeySalt.size());
		createLocalKey(QByteArray(), &_passKeySalt, &PassKey);

		EncryptedDescriptor passKeyData(kLocalKeySize);
		LocalKey->write(passKeyData.stream);
		_passKeyEncrypted = FileWriteDescriptor::prepareEncrypted(passKeyData, PassKey);
	}
	map.writeData(_passKeySalt);
	map.writeData(_passKeyEncrypted);

	uint32 mapSize = 0;
	const auto self = [] {
		if (!AuthSession::Exists()) {
			DEBUG_LOG(("AuthSelf Warning: Session does not exist."));
			return QByteArray();
		}
		const auto self = Auth().user();
		if (self->phone().isEmpty()) {
			DEBUG_LOG(("AuthSelf Error: Phone is empty."));
			return QByteArray();
		}
		auto result = QByteArray();
		result.reserve(Serialize::peerSize(self)
			+ Serialize::stringSize(self->about()));
		{
			QBuffer buffer(&result);
			buffer.open(QIODevice::WriteOnly);
			QDataStream stream(&buffer);
			Serialize::writePeer(stream, self);
			stream << self->about();
		}
		return result;
	}();
	if (!self.isEmpty()) mapSize += sizeof(quint32) + Serialize::bytearraySize(self);
	if (!_draftsMap.isEmpty()) mapSize += sizeof(quint32) * 2 + _draftsMap.size() * sizeof(quint64) * 2;
	if (!_draftCursorsMap.isEmpty()) mapSize += sizeof(quint32) * 2 + _draftCursorsMap.size() * sizeof(quint64) * 2;
	if (_locationsKey) mapSize += sizeof(quint32) + sizeof(quint64);
	if (_reportSpamStatusesKey) mapSize += sizeof(quint32) + sizeof(quint64);
	if (_trustedBotsKey) mapSize += sizeof(quint32) + sizeof(quint64);
	if (_recentStickersKeyOld) mapSize += sizeof(quint32) + sizeof(quint64);
	if (_installedStickersKey || _featuredStickersKey || _recentStickersKey || _archivedStickersKey) {
		mapSize += sizeof(quint32) + 4 * sizeof(quint64);
	}
	if (_favedStickersKey) mapSize += sizeof(quint32) + sizeof(quint64);
	if (_savedGifsKey) mapSize += sizeof(quint32) + sizeof(quint64);
	if (_savedPeersKey) mapSize += sizeof(quint32) + sizeof(quint64);
	if (_backgroundKeyDay || _backgroundKeyNight) mapSize += sizeof(quint32) + sizeof(quint64) + sizeof(quint64);
	if (_userSettingsKey) mapSize += sizeof(quint32) + sizeof(quint64);
	if (_recentHashtagsAndBotsKey) mapSize += sizeof(quint32) + sizeof(quint64);
	if (_exportSettingsKey) mapSize += sizeof(quint32) + sizeof(quint64);

	EncryptedDescriptor mapData(mapSize);
	if (!self.isEmpty()) {
		mapData.stream << quint32(lskSelfSerialized) << self;
	}
	if (!_draftsMap.isEmpty()) {
		mapData.stream << quint32(lskDraft) << quint32(_draftsMap.size());
		for (DraftsMap::const_iterator i = _draftsMap.cbegin(), e = _draftsMap.cend(); i != e; ++i) {
			mapData.stream << quint64(i.value()) << quint64(i.key());
		}
	}
	if (!_draftCursorsMap.isEmpty()) {
		mapData.stream << quint32(lskDraftPosition) << quint32(_draftCursorsMap.size());
		for (DraftsMap::const_iterator i = _draftCursorsMap.cbegin(), e = _draftCursorsMap.cend(); i != e; ++i) {
			mapData.stream << quint64(i.value()) << quint64(i.key());
		}
	}
	if (_locationsKey) {
		mapData.stream << quint32(lskLocations) << quint64(_locationsKey);
	}
	if (_reportSpamStatusesKey) {
		mapData.stream << quint32(lskReportSpamStatuses) << quint64(_reportSpamStatusesKey);
	}
	if (_trustedBotsKey) {
		mapData.stream << quint32(lskTrustedBots) << quint64(_trustedBotsKey);
	}
	if (_recentStickersKeyOld) {
		mapData.stream << quint32(lskRecentStickersOld) << quint64(_recentStickersKeyOld);
	}
	if (_installedStickersKey || _featuredStickersKey || _recentStickersKey || _archivedStickersKey) {
		mapData.stream << quint32(lskStickersKeys);
		mapData.stream << quint64(_installedStickersKey) << quint64(_featuredStickersKey) << quint64(_recentStickersKey) << quint64(_archivedStickersKey);
	}
	if (_favedStickersKey) {
		mapData.stream << quint32(lskFavedStickers) << quint64(_favedStickersKey);
	}
	if (_savedGifsKey) {
		mapData.stream << quint32(lskSavedGifs) << quint64(_savedGifsKey);
	}
	if (_savedPeersKey) {
		mapData.stream << quint32(lskSavedPeers) << quint64(_savedPeersKey);
	}
	if (_backgroundKeyDay || _backgroundKeyNight) {
		mapData.stream
			<< quint32(lskBackground)
			<< quint64(_backgroundKeyDay)
			<< quint64(_backgroundKeyNight);
	}
	if (_userSettingsKey) {
		mapData.stream << quint32(lskUserSettings) << quint64(_userSettingsKey);
	}
	if (_recentHashtagsAndBotsKey) {
		mapData.stream << quint32(lskRecentHashtagsAndBots) << quint64(_recentHashtagsAndBotsKey);
	}
	if (_exportSettingsKey) {
		mapData.stream << quint32(lskExportSettings) << quint64(_exportSettingsKey);
	}
	map.writeEncrypted(mapData);

	_mapChanged = false;
}

} // namespace

void finish() {
	if (_manager) {
		_writeMap(WriteMapWhen::Now);
		_manager->finish();
		_manager->deleteLater();
		_manager = 0;
		delete base::take(_localLoader);
	}
}

void loadTheme();
void readLangPack();

void start() {
	Expects(!_manager);

	_manager = new internal::Manager();
	_localLoader = new TaskQueue(kFileLoaderQueueStopTimeout);

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

	ReadSettingsContext context;
	FileReadDescriptor settingsData;
	if (!readFile(settingsData, cTestMode() ? qsl("settings_test") : qsl("settings"), FileOption::Safe)) {
		_readOldSettings(true, context);
		_readOldUserSettings(false, context); // needed further in _readUserSettings
		_readOldMtpData(false, context); // needed further in _readMtpData
		applyReadContext(std::move(context));

		return writeSettings();
	}
	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();
	}
	createLocalKey(QByteArray(), &salt, &SettingsKey);

	EncryptedDescriptor settings;
	if (!decryptLocal(settings, settingsEncrypted, SettingsKey)) {
		LOG(("App Error: could not decrypt settings from settings file, maybe bad passcode..."));
		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;

	loadTheme();
	readLangPack();

	applyReadContext(std::move(context));
}

void writeSettings() {
	if (_basePath.isEmpty()) {
		LOG(("App Error: _basePath is empty in writeSettings()"));
		return;
	}

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

	FileWriteDescriptor settings(cTestMode() ? qsl("settings_test") : qsl("settings"), FileOption::Safe);
	if (_settingsSalt.isEmpty() || !SettingsKey) {
		_settingsSalt.resize(LocalEncryptSaltSize);
		memset_rand(_settingsSalt.data(), _settingsSalt.size());
		createLocalKey(QByteArray(), &_settingsSalt, &SettingsKey);
	}
	settings.writeData(_settingsSalt);

	auto dcOptionsSerialized = Messenger::Instance().dcOptions()->serialize();

	quint32 size = 12 * (sizeof(quint32) + sizeof(qint32));
	size += sizeof(quint32) + Serialize::bytearraySize(dcOptionsSerialized);
	size += sizeof(quint32) + Serialize::stringSize(cLoggedPhoneNumber());
	size += sizeof(quint32) + Serialize::stringSize(Global::TxtDomainString());

	auto &proxies = Global::RefProxiesList();
	const auto &proxy = Global::SelectedProxy();
	auto proxyIt = ranges::find(proxies, proxy);
	if (proxy.type != ProxyData::Type::None
		&& proxyIt == end(proxies)) {
		proxies.push_back(proxy);
		proxyIt = end(proxies) - 1;
	}
	size += sizeof(quint32) + sizeof(qint32) + sizeof(qint32) + sizeof(qint32);
	for (const auto &proxy : proxies) {
		size += sizeof(qint32) + Serialize::stringSize(proxy.host) + sizeof(qint32) + Serialize::stringSize(proxy.user) + Serialize::stringSize(proxy.password);
	}

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

	EncryptedDescriptor data(size);
	data.stream << quint32(dbiChatSizeMax) << qint32(Global::ChatSizeMax());
	data.stream << quint32(dbiMegagroupSizeMax) << qint32(Global::MegagroupSizeMax());
	data.stream << quint32(dbiSavedGifsLimit) << qint32(Global::SavedGifsLimit());
	data.stream << quint32(dbiStickersRecentLimit) << qint32(Global::StickersRecentLimit());
	data.stream << quint32(dbiStickersFavedLimit) << qint32(Global::StickersFavedLimit());
	data.stream << quint32(dbiAutoStart) << qint32(cAutoStart());
	data.stream << quint32(dbiStartMinimized) << qint32(cStartMinimized());
	data.stream << quint32(dbiSendToMenu) << qint32(cSendToMenu());
	data.stream << quint32(dbiWorkMode) << qint32(Global::WorkMode().value());
	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(dbiDcOptions) << dcOptionsSerialized;
	data.stream << quint32(dbiLoggedPhoneNumber) << cLoggedPhoneNumber();
	data.stream << quint32(dbiTxtDomainString) << Global::TxtDomainString();
	data.stream << quint32(dbiAnimationsDisabled) << qint32(anim::Disabled() ? 1 : 0);

	data.stream << quint32(dbiConnectionType) << qint32(dbictProxiesList);
	data.stream << qint32(proxies.size());
	data.stream << qint32(proxyIt - begin(proxies)) + 1;
	data.stream << qint32(Global::ProxySettings());
	data.stream << qint32(Global::UseProxyForCalls() ? 1 : 0);
	for (const auto &proxy : proxies) {
		data.stream << qint32(kProxyTypeShift + int(proxy.type));
		data.stream << proxy.host << qint32(proxy.port) << proxy.user << proxy.password;
	}

	data.stream << quint32(dbiTryIPv6) << qint32(Global::TryIPv6());
	data.stream
		<< quint32(dbiThemeKey)
		<< quint64(_themeKeyDay)
		<< quint64(_themeKeyNight)
		<< quint32(Window::Theme::IsNightMode() ? 1 : 0);
	if (_langPackKey) {
		data.stream << quint32(dbiLangPackKey) << quint64(_langPackKey);
	}
	if (_languagesKey) {
		data.stream << quint32(dbiLanguagesKey) << quint64(_languagesKey);
	}

	auto position = cWindowPos();
	data.stream << quint32(dbiWindowPosition) << qint32(position.x) << qint32(position.y) << qint32(position.w) << qint32(position.h);
	data.stream << qint32(position.moncrc) << qint32(position.maximized);

	DEBUG_LOG(("Window Pos: Writing to storage %1, %2, %3, %4 (maximized %5)").arg(position.x).arg(position.y).arg(position.w).arg(position.h).arg(Logs::b(position.maximized)));

	settings.writeEncrypted(data, SettingsKey);
}

void writeUserSettings() {
	_writeUserSettings();
}

void writeMtpData() {
	_writeMtpData();
}

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://updates.tdesktop.com");
}

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 reset() {
	if (_localLoader) {
		_localLoader->stop();
	}

	_passKeySalt.clear(); // reset passcode, local key
	_draftsMap.clear();
	_draftCursorsMap.clear();
	_fileLocations.clear();
	_fileLocationPairs.clear();
	_fileLocationAliases.clear();
	_draftsNotReadMap.clear();
	_locationsKey = _reportSpamStatusesKey = _trustedBotsKey = 0;
	_recentStickersKeyOld = 0;
	_installedStickersKey = _featuredStickersKey = _recentStickersKey = _favedStickersKey = _archivedStickersKey = 0;
	_savedGifsKey = 0;
	_backgroundKeyDay = _backgroundKeyNight = 0;
	Window::Theme::Background()->reset();
	_userSettingsKey = _recentHashtagsAndBotsKey = _savedPeersKey = _exportSettingsKey = 0;
	_oldMapVersion = _oldSettingsVersion = 0;
	_cacheTotalSizeLimit = Database::Settings().totalSizeLimit;
	_cacheTotalTimeLimit = Database::Settings().totalTimeLimit;
	StoredAuthSessionCache.reset();
	_mapChanged = true;
	_writeMap(WriteMapWhen::Now);

	_writeMtpData();
}

bool checkPasscode(const QByteArray &passcode) {
	auto checkKey = MTP::AuthKeyPtr();
	createLocalKey(passcode, &_passKeySalt, &checkKey);
	return checkKey->equals(PassKey);
}

void setPasscode(const QByteArray &passcode) {
	createLocalKey(passcode, &_passKeySalt, &PassKey);

	EncryptedDescriptor passKeyData(kLocalKeySize);
	LocalKey->write(passKeyData.stream);
	_passKeyEncrypted = FileWriteDescriptor::prepareEncrypted(passKeyData, PassKey);

	_mapChanged = true;
	_writeMap(WriteMapWhen::Now);

	Global::SetLocalPasscode(!passcode.isEmpty());
	Global::RefLocalPasscodeChanged().notify();
}

base::flat_set<QString> CollectGoodNames() {
	const auto keys = {
		_locationsKey,
		_reportSpamStatusesKey,
		_userSettingsKey,
		_installedStickersKey,
		_featuredStickersKey,
		_recentStickersKey,
		_favedStickersKey,
		_archivedStickersKey,
		_recentStickersKeyOld,
		_savedGifsKey,
		_backgroundKeyNight,
		_backgroundKeyDay,
		_recentHashtagsAndBotsKey,
		_exportSettingsKey,
		_savedPeersKey,
		_trustedBotsKey
	};
	auto result = base::flat_set<QString>{ "map0", "map1" };
	const auto push = [&](FileKey key) {
		if (!key) {
			return;
		}
		auto name = toFilePart(key) + '0';
		result.emplace(name);
		name[name.size() - 1] = '1';
		result.emplace(name);
	};
	for (const auto &value : _draftsMap) {
		push(value);
	}
	for (const auto &value : _draftCursorsMap) {
		push(value);
	}
	for (const auto &value : keys) {
		push(value);
	}
	return result;
}

void FilterLegacyFiles(FnMut<void(base::flat_set<QString>&&)> then) {
	crl::on_main([then = std::move(then)]() mutable {
		then(CollectGoodNames());
	});
}

ReadMapState readMap(const QByteArray &pass) {
	ReadMapState result = _readMap(pass);
	if (result == ReadMapFailed) {
		_mapChanged = true;
		_writeMap(WriteMapWhen::Now);
	}
	if (result != ReadMapPassNeeded) {
		Storage::ClearLegacyFiles(_userBasePath, FilterLegacyFiles);
	}
	return result;
}

int32 oldMapVersion() {
	return _oldMapVersion;
}

int32 oldSettingsVersion() {
	return _oldSettingsVersion;
}

void writeDrafts(const PeerId &peer, const MessageDraft &localDraft, const MessageDraft &editDraft) {
	if (!_working()) return;

	if (localDraft.msgId <= 0 && localDraft.textWithTags.text.isEmpty() && editDraft.msgId <= 0) {
		auto i = _draftsMap.find(peer);
		if (i != _draftsMap.cend()) {
			clearKey(i.value());
			_draftsMap.erase(i);
			_mapChanged = true;
			_writeMap();
		}

		_draftsNotReadMap.remove(peer);
	} else {
		auto i = _draftsMap.constFind(peer);
		if (i == _draftsMap.cend()) {
			i = _draftsMap.insert(peer, genKey());
			_mapChanged = true;
			_writeMap(WriteMapWhen::Fast);
		}

		auto msgTags = TextUtilities::SerializeTags(
			localDraft.textWithTags.tags);
		auto editTags = TextUtilities::SerializeTags(
			editDraft.textWithTags.tags);

		int size = sizeof(quint64);
		size += Serialize::stringSize(localDraft.textWithTags.text) + Serialize::bytearraySize(msgTags) + 2 * sizeof(qint32);
		size += Serialize::stringSize(editDraft.textWithTags.text) + Serialize::bytearraySize(editTags) + 2 * sizeof(qint32);

		EncryptedDescriptor data(size);
		data.stream << quint64(peer);
		data.stream << localDraft.textWithTags.text << msgTags;
		data.stream << qint32(localDraft.msgId) << qint32(localDraft.previewCancelled ? 1 : 0);
		data.stream << editDraft.textWithTags.text << editTags;
		data.stream << qint32(editDraft.msgId) << qint32(editDraft.previewCancelled ? 1 : 0);

		FileWriteDescriptor file(i.value());
		file.writeEncrypted(data);

		_draftsNotReadMap.remove(peer);
	}
}

void clearDraftCursors(const PeerId &peer) {
	DraftsMap::iterator i = _draftCursorsMap.find(peer);
	if (i != _draftCursorsMap.cend()) {
		clearKey(i.value());
		_draftCursorsMap.erase(i);
		_mapChanged = true;
		_writeMap();
	}
}

void _readDraftCursors(const PeerId &peer, MessageCursor &localCursor, MessageCursor &editCursor) {
	DraftsMap::iterator j = _draftCursorsMap.find(peer);
	if (j == _draftCursorsMap.cend()) {
		return;
	}

	FileReadDescriptor draft;
	if (!readEncryptedFile(draft, j.value())) {
		clearDraftCursors(peer);
		return;
	}
	quint64 draftPeer;
	qint32 localPosition = 0, localAnchor = 0, localScroll = QFIXED_MAX;
	qint32 editPosition = 0, editAnchor = 0, editScroll = QFIXED_MAX;
	draft.stream >> draftPeer >> localPosition >> localAnchor >> localScroll;
	if (!draft.stream.atEnd()) {
		draft.stream >> editPosition >> editAnchor >> editScroll;
	}

	if (draftPeer != peer) {
		clearDraftCursors(peer);
		return;
	}

	localCursor = MessageCursor(localPosition, localAnchor, localScroll);
	editCursor = MessageCursor(editPosition, editAnchor, editScroll);
}

void readDraftsWithCursors(History *h) {
	PeerId peer = h->peer->id;
	if (!_draftsNotReadMap.remove(peer)) {
		clearDraftCursors(peer);
		return;
	}

	DraftsMap::iterator j = _draftsMap.find(peer);
	if (j == _draftsMap.cend()) {
		clearDraftCursors(peer);
		return;
	}
	FileReadDescriptor draft;
	if (!readEncryptedFile(draft, j.value())) {
		clearKey(j.value());
		_draftsMap.erase(j);
		clearDraftCursors(peer);
		return;
	}

	quint64 draftPeer = 0;
	TextWithTags msgData, editData;
	QByteArray msgTagsSerialized, editTagsSerialized;
	qint32 msgReplyTo = 0, msgPreviewCancelled = 0, editMsgId = 0, editPreviewCancelled = 0;
	draft.stream >> draftPeer >> msgData.text;
	if (draft.version >= 9048) {
		draft.stream >> msgTagsSerialized;
	}
	if (draft.version >= 7021) {
		draft.stream >> msgReplyTo;
		if (draft.version >= 8001) {
			draft.stream >> msgPreviewCancelled;
			if (!draft.stream.atEnd()) {
				draft.stream >> editData.text;
				if (draft.version >= 9048) {
					draft.stream >> editTagsSerialized;
				}
				draft.stream >> editMsgId >> editPreviewCancelled;
			}
		}
	}
	if (draftPeer != peer) {
		clearKey(j.value());
		_draftsMap.erase(j);
		clearDraftCursors(peer);
		return;
	}

	msgData.tags = TextUtilities::DeserializeTags(
		msgTagsSerialized,
		msgData.text.size());
	editData.tags = TextUtilities::DeserializeTags(
		editTagsSerialized,
		editData.text.size());

	MessageCursor msgCursor, editCursor;
	_readDraftCursors(peer, msgCursor, editCursor);

	if (!h->localDraft()) {
		if (msgData.text.isEmpty() && !msgReplyTo) {
			h->clearLocalDraft();
		} else {
			h->setLocalDraft(std::make_unique<Data::Draft>(
				msgData,
				msgReplyTo,
				msgCursor,
				msgPreviewCancelled));
		}
	}
	if (!editMsgId) {
		h->clearEditDraft();
	} else {
		h->setEditDraft(std::make_unique<Data::Draft>(
			editData,
			editMsgId,
			editCursor,
			editPreviewCancelled));
	}
}

void writeDraftCursors(const PeerId &peer, const MessageCursor &msgCursor, const MessageCursor &editCursor) {
	if (!_working()) return;

	if (msgCursor == MessageCursor() && editCursor == MessageCursor()) {
		clearDraftCursors(peer);
	} else {
		DraftsMap::const_iterator i = _draftCursorsMap.constFind(peer);
		if (i == _draftCursorsMap.cend()) {
			i = _draftCursorsMap.insert(peer, genKey());
			_mapChanged = true;
			_writeMap(WriteMapWhen::Fast);
		}

		EncryptedDescriptor data(sizeof(quint64) + sizeof(qint32) * 3);
		data.stream << quint64(peer) << qint32(msgCursor.position) << qint32(msgCursor.anchor) << qint32(msgCursor.scroll);
		data.stream << qint32(editCursor.position) << qint32(editCursor.anchor) << qint32(editCursor.scroll);

		FileWriteDescriptor file(i.value());
		file.writeEncrypted(data);
	}
}

bool hasDraftCursors(const PeerId &peer) {
	return _draftCursorsMap.contains(peer);
}

bool hasDraft(const PeerId &peer) {
	return _draftsMap.contains(peer);
}

void writeFileLocation(MediaKey location, const FileLocation &local) {
	if (local.fname.isEmpty()) return;

	FileLocationAliases::const_iterator aliasIt = _fileLocationAliases.constFind(location);
	if (aliasIt != _fileLocationAliases.cend()) {
		location = aliasIt.value();
	}

	FileLocationPairs::iterator i = _fileLocationPairs.find(local.fname);
	if (i != _fileLocationPairs.cend()) {
		if (i.value().second == local) {
			if (i.value().first != location) {
				_fileLocationAliases.insert(location, i.value().first);
				_writeLocations(WriteMapWhen::Fast);
			}
			return;
		}
		if (i.value().first != location) {
			for (FileLocations::iterator j = _fileLocations.find(i.value().first), e = _fileLocations.end(); (j != e) && (j.key() == i.value().first);) {
				if (j.value() == i.value().second) {
					_fileLocations.erase(j);
					break;
				}
			}
			_fileLocationPairs.erase(i);
		}
	}
	_fileLocations.insert(location, local);
	_fileLocationPairs.insert(local.fname, FileLocationPair(location, local));
	_writeLocations(WriteMapWhen::Fast);
}

FileLocation readFileLocation(MediaKey location, bool check) {
	FileLocationAliases::const_iterator aliasIt = _fileLocationAliases.constFind(location);
	if (aliasIt != _fileLocationAliases.cend()) {
		location = aliasIt.value();
	}

	FileLocations::iterator i = _fileLocations.find(location);
	for (FileLocations::iterator i = _fileLocations.find(location); (i != _fileLocations.end()) && (i.key() == location);) {
		if (check) {
			if (!i.value().check()) {
				_fileLocationPairs.remove(i.value().fname);
				i = _fileLocations.erase(i);
				_writeLocations();
				continue;
			}
		}
		return i.value();
	}
	return FileLocation();
}

qint32 _storageImageSize(qint32 rawlen) {
	// fulllen + storagekey + type + len + data
	qint32 result = sizeof(uint32) + sizeof(quint64) * 2 + sizeof(quint32) + sizeof(quint32) + rawlen;
	if (result & 0x0F) result += 0x10 - (result & 0x0F);
	result += tdfMagicLen + sizeof(qint32) + sizeof(quint32) + 0x10 + 0x10; // magic + version + len of encrypted + part of sha1 + md5
	return result;
}

qint32 _storageStickerSize(qint32 rawlen) {
	// fulllen + storagekey + len + data
	qint32 result = sizeof(uint32) + sizeof(quint64) * 2 + sizeof(quint32) + rawlen;
	if (result & 0x0F) result += 0x10 - (result & 0x0F);
	result += tdfMagicLen + sizeof(qint32) + sizeof(quint32) + 0x10 + 0x10; // magic + version + len of encrypted + part of sha1 + md5
	return result;
}

qint32 _storageAudioSize(qint32 rawlen) {
	// fulllen + storagekey + len + data
	qint32 result = sizeof(uint32) + sizeof(quint64) * 2 + sizeof(quint32) + rawlen;
	if (result & 0x0F) result += 0x10 - (result & 0x0F);
	result += tdfMagicLen + sizeof(qint32) + sizeof(quint32) + 0x10 + 0x10; // magic + version + len of encrypted + part of sha1 + md5
	return result;
}

QString cachePath() {
	Expects(!_userDbPath.isEmpty());

	return _userDbPath + "cache";
}

Storage::EncryptionKey cacheKey() {
	Expects(LocalKey != nullptr);

	return Storage::EncryptionKey(bytes::make_vector(LocalKey->data()));
}

Storage::Cache::Database::Settings cacheSettings() {
	auto result = Storage::Cache::Database::Settings();
	result.clearOnWrongKey = true;
	result.totalSizeLimit = _cacheTotalSizeLimit;
	result.totalTimeLimit = _cacheTotalTimeLimit;
	result.maxDataSize = Storage::kMaxFileInMemory;
	return result;
}

void updateCacheSettings(Storage::Cache::Database::SettingsUpdate &update) {
	Expects(update.totalSizeLimit > Database::Settings().maxDataSize);
	Expects(update.totalTimeLimit >= 0);

	if (_cacheTotalSizeLimit == update.totalSizeLimit
		&& _cacheTotalTimeLimit == update.totalTimeLimit) {
		return;
	}
	_cacheTotalSizeLimit = update.totalSizeLimit;
	_cacheTotalTimeLimit = update.totalTimeLimit;
	_writeUserSettings();
}

class CountWaveformTask : public Task {
public:
	CountWaveformTask(DocumentData *doc)
		: _doc(doc)
		, _loc(doc->location(true))
		, _data(doc->data())
		, _wavemax(0) {
		if (_data.isEmpty() && !_loc.accessEnable()) {
			_doc = 0;
		}
	}
	void process() {
		if (!_doc) return;

		_waveform = audioCountWaveform(_loc, _data);
		uchar wavemax = 0;
		for (int32 i = 0, l = _waveform.size(); i < l; ++i) {
			uchar waveat = _waveform.at(i);
			if (wavemax < waveat) wavemax = waveat;
		}
		_wavemax = wavemax;
	}
	void finish() {
		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;
			}
			Auth().data().requestDocumentViewRepaint(_doc);
		}
	}
	virtual ~CountWaveformTask() {
		if (_data.isEmpty() && _doc) {
			_loc.accessDisable();
		}
	}

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

};

void countVoiceWaveform(DocumentData *document) {
	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>(document));
			memcpy(voice->waveform.data() + 1, &taskId, sizeof(taskId));
		}
	}
}

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

void _writeStickerSet(QDataStream &stream, const Stickers::Set &set) {
	bool notLoaded = (set.flags & MTPDstickerSet_ClientFlag::f_not_loaded);
	if (notLoaded) {
		stream
			<< quint64(set.id)
			<< quint64(set.access)
			<< set.title
			<< set.shortName
			<< qint32(-set.count)
			<< qint32(set.hash)
			<< qint32(set.flags);
		if (AppVersion > 1002008) {
			stream << qint32(set.installDate);
		}
		return;
	} else {
		if (set.stickers.isEmpty()) return;
	}

	stream
		<< quint64(set.id)
		<< quint64(set.access)
		<< set.title
		<< set.shortName
		<< qint32(set.stickers.size())
		<< qint32(set.hash)
		<< qint32(set.flags);
	if (AppVersion > 1002008) {
		stream << qint32(set.installDate);
	}
	for (auto j = set.stickers.cbegin(), e = set.stickers.cend(); j != e; ++j) {
		Serialize::Document::writeToStream(stream, *j);
	}
	if (AppVersion > 1002008) {
		stream << qint32(set.dates.size());
		if (!set.dates.empty()) {
			Assert(set.dates.size() == set.stickers.size());
			for (const auto date : set.dates) {
				stream << qint32(date);
			}
		}
	}

	if (AppVersion > 9018) {
		stream << qint32(set.emoji.size());
		for (auto j = set.emoji.cbegin(), e = set.emoji.cend(); j != e; ++j) {
			stream << j.key()->id() << qint32(j->size());
			for (int32 k = 0, l = j->size(); k < l; ++k) {
				stream << quint64(j->at(k)->id);
			}
		}
	}
}

// In generic method _writeStickerSets() we look through all the sets and call a
// callback on each set to see, if we write it, skip it or abort the whole write.
enum class StickerSetCheckResult {
	Write,
	Skip,
	Abort,
};

// CheckSet is a functor on Stickers::Set, which returns a StickerSetCheckResult.
template <typename CheckSet>
void _writeStickerSets(FileKey &stickersKey, CheckSet checkSet, const Stickers::Order &order) {
	if (!_working()) return;

	const auto &sets = Auth().data().stickerSets();
	if (sets.isEmpty()) {
		if (stickersKey) {
			clearKey(stickersKey);
			stickersKey = 0;
			_mapChanged = true;
		}
		_writeMap();
		return;
	}
	int32 setsCount = 0;
	QByteArray hashToWrite;
	quint32 size = sizeof(quint32) + Serialize::bytearraySize(hashToWrite);
	for (const auto &set : sets) {
		auto result = checkSet(set);
		if (result == StickerSetCheckResult::Abort) {
			return;
		} else if (result == StickerSetCheckResult::Skip) {
			continue;
		}

		// id + access + title + shortName + stickersCount + hash + flags + installDate
		size += sizeof(quint64) * 2 + Serialize::stringSize(set.title) + Serialize::stringSize(set.shortName) + sizeof(quint32) + sizeof(qint32) * 3;
		for (const auto sticker : std::as_const(set.stickers)) {
			sticker->refreshStickerThumbFileReference();
			size += Serialize::Document::sizeInStream(sticker);
		}
		size += sizeof(qint32); // dates count
		if (!set.dates.empty()) {
			Assert(set.stickers.size() == set.dates.size());
			size += set.dates.size() * sizeof(qint32);
		}

		size += sizeof(qint32); // emojiCount
		for (auto j = set.emoji.cbegin(), e = set.emoji.cend(); j != e; ++j) {
			size += Serialize::stringSize(j.key()->id()) + sizeof(qint32) + (j->size() * sizeof(quint64));
		}

		++setsCount;
	}
	if (!setsCount && order.isEmpty()) {
		if (stickersKey) {
			clearKey(stickersKey);
			stickersKey = 0;
			_mapChanged = true;
		}
		_writeMap();
		return;
	}
	size += sizeof(qint32) + (order.size() * sizeof(quint64));

	if (!stickersKey) {
		stickersKey = genKey();
		_mapChanged = true;
		_writeMap(WriteMapWhen::Fast);
	}
	EncryptedDescriptor data(size);
	data.stream << quint32(setsCount) << hashToWrite;
	for_const (auto &set, sets) {
		auto result = checkSet(set);
		if (result == StickerSetCheckResult::Abort) {
			return;
		} else if (result == StickerSetCheckResult::Skip) {
			continue;
		}
		_writeStickerSet(data.stream, set);
	}
	data.stream << order;

	FileWriteDescriptor file(stickersKey);
	file.writeEncrypted(data);
}

void _readStickerSets(FileKey &stickersKey, Stickers::Order *outOrder = nullptr, MTPDstickerSet::Flags readingFlags = 0) {
	FileReadDescriptor stickers;
	if (!readEncryptedFile(stickers, stickersKey)) {
		clearKey(stickersKey);
		stickersKey = 0;
		_writeMap();
		return;
	}

	bool readingInstalled = (readingFlags == MTPDstickerSet::Flag::f_installed_date);

	auto &sets = Auth().data().stickerSetsRef();
	if (outOrder) outOrder->clear();

	quint32 cnt;
	QByteArray hash;
	stickers.stream >> cnt >> hash; // ignore hash, it is counted
	if (readingInstalled && stickers.version < 8019) { // bad data in old caches
		cnt += 2; // try to read at least something
	}
	for (uint32 i = 0; i < cnt; ++i) {
		quint64 setId = 0, setAccess = 0;
		QString setTitle, setShortName;
		qint32 scnt = 0;
		auto setInstallDate = qint32(0);

		stickers.stream
			>> setId
			>> setAccess
			>> setTitle
			>> setShortName
			>> scnt;

		qint32 setHash = 0;
		MTPDstickerSet::Flags setFlags = 0;
		if (stickers.version > 8033) {
			qint32 setFlagsValue = 0;
			stickers.stream >> setHash >> setFlagsValue;
			setFlags = MTPDstickerSet::Flags::from_raw(setFlagsValue);
			if (setFlags & MTPDstickerSet_ClientFlag::f_not_loaded__old) {
				setFlags &= ~MTPDstickerSet_ClientFlag::f_not_loaded__old;
				setFlags |= MTPDstickerSet_ClientFlag::f_not_loaded;
			}
		}
		if (stickers.version > 1002008) {
			stickers.stream >> setInstallDate;
		}
		if (readingInstalled && stickers.version < 9061) {
			setFlags |= MTPDstickerSet::Flag::f_installed_date;
		}

		if (setId == Stickers::DefaultSetId) {
			setTitle = lang(lng_stickers_default_set);
			setFlags |= MTPDstickerSet::Flag::f_official | MTPDstickerSet_ClientFlag::f_special;
			if (readingInstalled && outOrder && stickers.version < 9061) {
				outOrder->push_front(setId);
			}
		} else if (setId == Stickers::CustomSetId) {
			setTitle = qsl("Custom stickers");
			setFlags |= MTPDstickerSet_ClientFlag::f_special;
		} else if (setId == Stickers::CloudRecentSetId) {
			setTitle = lang(lng_recent_stickers);
			setFlags |= MTPDstickerSet_ClientFlag::f_special;
		} else if (setId == Stickers::FavedSetId) {
			setTitle = Lang::Hard::FavedSetTitle();
			setFlags |= MTPDstickerSet_ClientFlag::f_special;
		} else if (setId) {
			if (readingInstalled && outOrder && stickers.version < 9061) {
				outOrder->push_back(setId);
			}
		} else {
			continue;
		}

		auto it = sets.find(setId);
		if (it == sets.cend()) {
			// We will set this flags from order lists when reading those stickers.
			setFlags &= ~(MTPDstickerSet::Flag::f_installed_date | MTPDstickerSet_ClientFlag::f_featured);
			it = sets.insert(setId, Stickers::Set(
				setId,
				setAccess,
				setTitle,
				setShortName,
				0,
				setHash,
				MTPDstickerSet::Flags(setFlags),
				setInstallDate));
		}
		auto &set = it.value();
		auto inputSet = MTP_inputStickerSetID(MTP_long(set.id), MTP_long(set.access));

		if (scnt < 0) { // disabled not loaded set
			if (!set.count || set.stickers.isEmpty()) {
				set.count = -scnt;
			}
			continue;
		}

		bool fillStickers = set.stickers.isEmpty();
		if (fillStickers) {
			set.stickers.reserve(scnt);
			set.count = 0;
		}

		Serialize::Document::StickerSetInfo info(setId, setAccess, setShortName);
		OrderedSet<DocumentId> read;
		for (int32 j = 0; j < scnt; ++j) {
			auto document = Serialize::Document::readStickerFromStream(stickers.version, stickers.stream, info);
			if (!document || !document->sticker()) continue;

			if (read.contains(document->id)) continue;
			read.insert(document->id);

			if (fillStickers) {
				set.stickers.push_back(document);
				if (!(set.flags & MTPDstickerSet_ClientFlag::f_special)) {
					if (document->sticker()->set.type() != mtpc_inputStickerSetID) {
						document->sticker()->set = inputSet;
					}
				}
				++set.count;
			}
		}

		if (stickers.version > 1002008) {
			auto datesCount = qint32(0);
			stickers.stream >> datesCount;
			if (datesCount > 0) {
				if (datesCount != scnt) {
					// Bad file.
					return;
				}
				set.dates.reserve(datesCount);
				for (auto i = 0; i != datesCount; ++i) {
					auto date = qint32();
					stickers.stream >> date;
					if (set.id == Stickers::CloudRecentSetId) {
						set.dates.push_back(TimeId(date));
					}
				}
			}
		}

		if (stickers.version > 9018) {
			qint32 emojiCount;
			stickers.stream >> emojiCount;
			for (int32 j = 0; j < emojiCount; ++j) {
				QString emojiString;
				qint32 stickersCount;
				stickers.stream >> emojiString >> stickersCount;
				Stickers::Pack pack;
				pack.reserve(stickersCount);
				for (int32 k = 0; k < stickersCount; ++k) {
					quint64 id;
					stickers.stream >> id;
					const auto doc = Auth().data().document(id);
					if (!doc->sticker()) continue;

					pack.push_back(doc);
				}
				if (fillStickers) {
					if (auto emoji = Ui::Emoji::Find(emojiString)) {
						emoji = emoji->original();
						set.emoji.insert(emoji, pack);
					}
				}
			}
		}
	}

	// Read orders of installed and featured stickers.
	if (outOrder && stickers.version >= 9061) {
		stickers.stream >> *outOrder;
	}

	// Set flags that we dropped above from the order.
	if (readingFlags && outOrder) {
		for_const (auto setId, *outOrder) {
			auto it = sets.find(setId);
			if (it != sets.cend()) {
				it->flags |= readingFlags;
				if (readingInstalled && !it->installDate) {
					it->installDate = kDefaultStickerInstallDate;
				}
			}
		}
	}
}

void writeInstalledStickers() {
	if (!Global::started()) return;

	_writeStickerSets(_installedStickersKey, [](const Stickers::Set &set) {
		if (set.id == Stickers::CloudRecentSetId || set.id == Stickers::FavedSetId) { // separate files for them
			return StickerSetCheckResult::Skip;
		} else if (set.flags & MTPDstickerSet_ClientFlag::f_special) {
			if (set.stickers.isEmpty()) { // all other special are "installed"
				return StickerSetCheckResult::Skip;
			}
		} else if (!(set.flags & MTPDstickerSet::Flag::f_installed_date) || (set.flags & MTPDstickerSet::Flag::f_archived)) {
			return StickerSetCheckResult::Skip;
		} else if (set.flags & MTPDstickerSet_ClientFlag::f_not_loaded) { // waiting to receive
			return StickerSetCheckResult::Abort;
		} else if (set.stickers.isEmpty()) {
			return StickerSetCheckResult::Skip;
		}
		return StickerSetCheckResult::Write;
	}, Auth().data().stickerSetsOrder());
}

void writeFeaturedStickers() {
	if (!Global::started()) return;

	_writeStickerSets(_featuredStickersKey, [](const Stickers::Set &set) {
		if (set.id == Stickers::CloudRecentSetId || set.id == Stickers::FavedSetId) { // separate files for them
			return StickerSetCheckResult::Skip;
		} else if (set.flags & MTPDstickerSet_ClientFlag::f_special) {
			return StickerSetCheckResult::Skip;
		} else if (!(set.flags & MTPDstickerSet_ClientFlag::f_featured)) {
			return StickerSetCheckResult::Skip;
		} else if (set.flags & MTPDstickerSet_ClientFlag::f_not_loaded) { // waiting to receive
			return StickerSetCheckResult::Abort;
		} else if (set.stickers.isEmpty()) {
			return StickerSetCheckResult::Skip;
		}
		return StickerSetCheckResult::Write;
	}, Auth().data().featuredStickerSetsOrder());
}

void writeRecentStickers() {
	if (!Global::started()) return;

	_writeStickerSets(_recentStickersKey, [](const Stickers::Set &set) {
		if (set.id != Stickers::CloudRecentSetId || set.stickers.isEmpty()) {
			return StickerSetCheckResult::Skip;
		}
		return StickerSetCheckResult::Write;
	}, Stickers::Order());
}

void writeFavedStickers() {
	if (!Global::started()) return;

	_writeStickerSets(_favedStickersKey, [](const Stickers::Set &set) {
		if (set.id != Stickers::FavedSetId || set.stickers.isEmpty()) {
			return StickerSetCheckResult::Skip;
		}
		return StickerSetCheckResult::Write;
	}, Stickers::Order());
}

void writeArchivedStickers() {
	if (!Global::started()) return;

	_writeStickerSets(_archivedStickersKey, [](const Stickers::Set &set) {
		if (!(set.flags & MTPDstickerSet::Flag::f_archived) || set.stickers.isEmpty()) {
			return StickerSetCheckResult::Skip;
		}
		return StickerSetCheckResult::Write;
	}, Auth().data().archivedStickerSetsOrder());
}

void importOldRecentStickers() {
	if (!_recentStickersKeyOld) return;

	FileReadDescriptor stickers;
	if (!readEncryptedFile(stickers, _recentStickersKeyOld)) {
		clearKey(_recentStickersKeyOld);
		_recentStickersKeyOld = 0;
		_writeMap();
		return;
	}

	auto &sets = Auth().data().stickerSetsRef();
	sets.clear();

	auto &order = Auth().data().stickerSetsOrderRef();
	order.clear();

	auto &recent = cRefRecentStickers();
	recent.clear();

	auto &def = sets.insert(Stickers::DefaultSetId, Stickers::Set(
		Stickers::DefaultSetId,
		uint64(0),
		lang(lng_stickers_default_set),
		QString(),
		0, // count
		0, // hash
		(MTPDstickerSet::Flag::f_official
			| MTPDstickerSet::Flag::f_installed_date
			| MTPDstickerSet_ClientFlag::f_special),
		kDefaultStickerInstallDate)).value();
	auto &custom = sets.insert(Stickers::CustomSetId, Stickers::Set(
		Stickers::CustomSetId,
		uint64(0),
		qsl("Custom stickers"),
		QString(),
		0, // count
		0, // hash
		(MTPDstickerSet::Flag::f_installed_date
			| MTPDstickerSet_ClientFlag::f_special),
		kDefaultStickerInstallDate)).value();

	QMap<uint64, bool> read;
	while (!stickers.stream.atEnd()) {
		quint64 id, access;
		QString name, mime, alt;
		qint32 date, dc, size, width, height, type;
		qint16 value;
		stickers.stream >> id >> value >> access >> date >> name >> mime >> dc >> size >> width >> height >> type;
		if (stickers.version >= 7021) {
			stickers.stream >> alt;
		}
		if (!value || read.contains(id)) continue;
		read.insert(id, true);

		QVector<MTPDocumentAttribute> attributes;
		if (!name.isEmpty()) attributes.push_back(MTP_documentAttributeFilename(MTP_string(name)));
		if (type == AnimatedDocument) {
			attributes.push_back(MTP_documentAttributeAnimated());
		} else if (type == StickerDocument) {
			attributes.push_back(MTP_documentAttributeSticker(MTP_flags(0), MTP_string(alt), MTP_inputStickerSetEmpty(), MTPMaskCoords()));
		}
		if (width > 0 && height > 0) {
			attributes.push_back(MTP_documentAttributeImageSize(MTP_int(width), MTP_int(height)));
		}

		const auto doc = Auth().data().document(
			id,
			access,
			QByteArray(),
			date,
			attributes,
			mime,
			ImagePtr(),
			dc,
			size,
			StorageImageLocation());
		if (!doc->sticker()) continue;

		if (value > 0) {
			def.stickers.push_back(doc);
			++def.count;
		} else {
			custom.stickers.push_back(doc);
			++custom.count;
		}
		if (recent.size() < Global::StickersRecentLimit() && qAbs(value) > 1) {
			recent.push_back(qMakePair(doc, qAbs(value)));
		}
	}
	if (def.stickers.isEmpty()) {
		sets.remove(Stickers::DefaultSetId);
	} else {
		order.push_front(Stickers::DefaultSetId);
	}
	if (custom.stickers.isEmpty()) sets.remove(Stickers::CustomSetId);

	writeInstalledStickers();
	writeUserSettings();

	clearKey(_recentStickersKeyOld);
	_recentStickersKeyOld = 0;
	_writeMap();
}

void readInstalledStickers() {
	if (!_installedStickersKey) {
		return importOldRecentStickers();
	}

	Auth().data().stickerSetsRef().clear();
	_readStickerSets(
		_installedStickersKey,
		&Auth().data().stickerSetsOrderRef(),
		MTPDstickerSet::Flag::f_installed_date);
}

void readFeaturedStickers() {
	_readStickerSets(
		_featuredStickersKey,
		&Auth().data().featuredStickerSetsOrderRef(),
		MTPDstickerSet::Flags() | MTPDstickerSet_ClientFlag::f_featured);

	auto &sets = Auth().data().stickerSets();
	int unreadCount = 0;
	for_const (auto setId, Auth().data().featuredStickerSetsOrder()) {
		auto it = sets.constFind(setId);
		if (it != sets.cend() && (it->flags & MTPDstickerSet_ClientFlag::f_unread)) {
			++unreadCount;
		}
	}
	Auth().data().setFeaturedStickerSetsUnreadCount(unreadCount);
}

void readRecentStickers() {
	_readStickerSets(_recentStickersKey);
}

void readFavedStickers() {
	_readStickerSets(_favedStickersKey);
}

void readArchivedStickers() {
	static bool archivedStickersRead = false;
	if (!archivedStickersRead) {
		_readStickerSets(_archivedStickersKey, &Auth().data().archivedStickerSetsOrderRef());
		archivedStickersRead = true;
	}
}

int32 countDocumentVectorHash(const QVector<DocumentData*> vector) {
	uint32 acc = 0;
	for_const (auto doc, vector) {
		auto docId = doc->id;
		acc = (acc * 20261) + uint32(docId >> 32);
		acc = (acc * 20261) + uint32(docId & 0xFFFFFFFF);
	}
	return int32(acc & 0x7FFFFFFF);
}

int32 countSpecialStickerSetHash(uint64 setId) {
	auto &sets = Auth().data().stickerSets();
	auto it = sets.constFind(setId);
	if (it != sets.cend()) {
		return countDocumentVectorHash(it->stickers);
	}
	return 0;
}

int32 countStickersHash(bool checkOutdatedInfo) {
	uint32 acc = 0;
	bool foundOutdated = false;
	auto &sets = Auth().data().stickerSets();
	auto &order = Auth().data().stickerSetsOrder();
	for (auto i = order.cbegin(), e = order.cend(); i != e; ++i) {
		auto j = sets.constFind(*i);
		if (j != sets.cend()) {
			if (j->id == Stickers::DefaultSetId) {
				foundOutdated = true;
			} else if (!(j->flags & MTPDstickerSet_ClientFlag::f_special)
				&& !(j->flags & MTPDstickerSet::Flag::f_archived)) {
				acc = (acc * 20261) + j->hash;
			}
		}
	}
	return (!checkOutdatedInfo || !foundOutdated) ? int32(acc & 0x7FFFFFFF) : 0;
}

int32 countRecentStickersHash() {
	return countSpecialStickerSetHash(Stickers::CloudRecentSetId);
}

int32 countFavedStickersHash() {
	return countSpecialStickerSetHash(Stickers::FavedSetId);
}

int32 countFeaturedStickersHash() {
	uint32 acc = 0;
	auto &sets = Auth().data().stickerSets();
	auto &featured = Auth().data().featuredStickerSetsOrder();
	for_const (auto setId, featured) {
		acc = (acc * 20261) + uint32(setId >> 32);
		acc = (acc * 20261) + uint32(setId & 0xFFFFFFFF);

		auto it = sets.constFind(setId);
		if (it != sets.cend() && (it->flags & MTPDstickerSet_ClientFlag::f_unread)) {
			acc = (acc * 20261) + 1U;
		}
	}
	return int32(acc & 0x7FFFFFFF);
}

int32 countSavedGifsHash() {
	return countDocumentVectorHash(Auth().data().savedGifs());
}

void writeSavedGifs() {
	if (!_working()) return;

	auto &saved = Auth().data().savedGifs();
	if (saved.isEmpty()) {
		if (_savedGifsKey) {
			clearKey(_savedGifsKey);
			_savedGifsKey = 0;
			_mapChanged = true;
		}
		_writeMap();
	} else {
		quint32 size = sizeof(quint32); // count
		for_const (auto gif, saved) {
			size += Serialize::Document::sizeInStream(gif);
		}

		if (!_savedGifsKey) {
			_savedGifsKey = genKey();
			_mapChanged = true;
			_writeMap(WriteMapWhen::Fast);
		}
		EncryptedDescriptor data(size);
		data.stream << quint32(saved.size());
		for_const (auto gif, saved) {
			Serialize::Document::writeToStream(data.stream, gif);
		}
		FileWriteDescriptor file(_savedGifsKey);
		file.writeEncrypted(data);
	}
}

void readSavedGifs() {
	if (!_savedGifsKey) return;

	FileReadDescriptor gifs;
	if (!readEncryptedFile(gifs, _savedGifsKey)) {
		clearKey(_savedGifsKey);
		_savedGifsKey = 0;
		_writeMap();
		return;
	}

	auto &saved = Auth().data().savedGifsRef();
	saved.clear();

	quint32 cnt;
	gifs.stream >> cnt;
	saved.reserve(cnt);
	OrderedSet<DocumentId> read;
	for (uint32 i = 0; i < cnt; ++i) {
		auto document = Serialize::Document::readFromStream(gifs.version, gifs.stream);
		if (!document || !document->isGifv()) continue;

		if (read.contains(document->id)) continue;
		read.insert(document->id);

		saved.push_back(document);
	}
}

void writeBackground(int32 id, const QImage &img) {
	if (!_working() || !_backgroundCanWrite) {
		return;
	}

	if (!LocalKey) {
		LOG(("App Error: localkey not created in writeBackground()"));
		return;
	}

	auto &backgroundKey = Window::Theme::IsNightMode()
		? _backgroundKeyNight
		: _backgroundKeyDay;
	QByteArray bmp;
	if (!img.isNull()) {
		QBuffer buf(&bmp);
		if (!img.save(&buf, "BMP")) {
			return;
		}
	}
	if (!backgroundKey) {
		backgroundKey = genKey();
		_mapChanged = true;
		_writeMap(WriteMapWhen::Fast);
	}
	quint32 size = sizeof(qint32)
		+ sizeof(quint32)
		+ (bmp.isEmpty() ? 0 : (sizeof(quint32) + bmp.size()));
	EncryptedDescriptor data(size);
	data.stream << qint32(id) << bmp;

	FileWriteDescriptor file(backgroundKey);
	file.writeEncrypted(data);
}

bool readBackground() {
	FileReadDescriptor bg;
	auto &backgroundKey = Window::Theme::IsNightMode()
		? _backgroundKeyNight
		: _backgroundKeyDay;
	if (!readEncryptedFile(bg, backgroundKey)) {
		if (backgroundKey) {
			clearKey(backgroundKey);
			backgroundKey = 0;
			_mapChanged = true;
			_writeMap();
		}
		return false;
	}

	QByteArray bmpData;
	qint32 id;
	bg.stream >> id >> bmpData;
	auto oldEmptyImage = (bg.stream.status() != QDataStream::Ok);
	if (oldEmptyImage
		|| id == Window::Theme::kInitialBackground
		|| id == Window::Theme::kDefaultBackground) {
		_backgroundCanWrite = false;
		if (oldEmptyImage || bg.version < 8005) {
			Window::Theme::Background()->setImage(Window::Theme::kDefaultBackground);
			Window::Theme::Background()->setTile(false);
		} else {
			Window::Theme::Background()->setImage(id);
		}
		_backgroundCanWrite = true;
		return true;
	} else if (id == Window::Theme::kThemeBackground && bmpData.isEmpty()) {
		_backgroundCanWrite = false;
		Window::Theme::Background()->setImage(id);
		_backgroundCanWrite = true;
		return true;
	}

	QImage image;
	QBuffer buf(&bmpData);
	QImageReader reader(&buf);
#ifndef OS_MAC_OLD
	reader.setAutoTransform(true);
#endif // OS_MAC_OLD
	if (reader.read(&image)) {
		_backgroundCanWrite = false;
		Window::Theme::Background()->setImage(id, std::move(image));
		_backgroundCanWrite = true;
		return true;
	}
	return false;
}

Window::Theme::Saved readThemeUsingKey(FileKey key) {
	FileReadDescriptor theme;
	if (!readEncryptedFile(theme, key, FileOption::Safe, SettingsKey)) {
		return {};
	}

	auto result = Window::Theme::Saved();
	theme.stream >> result.content;
	theme.stream >> result.pathRelative >> result.pathAbsolute;
	if (theme.stream.status() != QDataStream::Ok) {
		return {};
	}

	QFile file(result.pathRelative);
	if (result.pathRelative.isEmpty() || !file.exists()) {
		file.setFileName(result.pathAbsolute);
	}

	auto changed = false;
	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 (result.content != fileContent) {
			result.content = fileContent;
			changed = true;
		}
	}
	if (!changed) {
		quint32 backgroundIsTiled = 0;
		theme.stream
			>> result.cache.paletteChecksum
			>> result.cache.contentChecksum
			>> result.cache.colors
			>> result.cache.background
			>> backgroundIsTiled;
		result.cache.tiled = (backgroundIsTiled == 1);
		if (theme.stream.status() != QDataStream::Ok) {
			return {};
		}
	}
	return result;
}

QString loadThemeUsingKey(FileKey key) {
	auto read = readThemeUsingKey(key);
	const auto result = read.pathAbsolute;
	return (!read.content.isEmpty() && Window::Theme::Load(std::move(read)))
		? result
		: QString();
}

void writeTheme(const Window::Theme::Saved &saved) {
	if (_themeKeyLegacy) {
		return;
	}
	auto &themeKey = Window::Theme::IsNightMode()
		? _themeKeyNight
		: _themeKeyDay;
	if (saved.content.isEmpty()) {
		if (themeKey) {
			clearKey(themeKey);
			themeKey = 0;
			writeSettings();
		}
		return;
	}

	if (!themeKey) {
		themeKey = genKey(FileOption::Safe);
		writeSettings();
	}

	auto backgroundTiled = static_cast<quint32>(saved.cache.tiled ? 1 : 0);
	quint32 size = Serialize::bytearraySize(saved.content);
	size += Serialize::stringSize(saved.pathRelative) + Serialize::stringSize(saved.pathAbsolute);
	size += sizeof(int32) * 2 + Serialize::bytearraySize(saved.cache.colors) + Serialize::bytearraySize(saved.cache.background) + sizeof(quint32);
	EncryptedDescriptor data(size);
	data.stream << saved.content;
	data.stream << saved.pathRelative << saved.pathAbsolute;
	data.stream << saved.cache.paletteChecksum << saved.cache.contentChecksum << saved.cache.colors << saved.cache.background << backgroundTiled;

	FileWriteDescriptor file(themeKey, FileOption::Safe);
	file.writeEncrypted(data, SettingsKey);
}

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

void loadTheme() {
	const auto key = (_themeKeyLegacy != 0)
		? _themeKeyLegacy
		: (Window::Theme::IsNightMode()
			? _themeKeyNight
			: _themeKeyDay);
	if (!key) {
		return;
	} else if (const auto path = loadThemeUsingKey(key); !path.isEmpty()) {
		if (_themeKeyLegacy) {
			Window::Theme::SetNightModeValue(path
				== Window::Theme::NightThemePath());
			(Window::Theme::IsNightMode()
				? _themeKeyNight
				: _themeKeyDay) = base::take(_themeKeyLegacy);
		}
	} else {
		clearTheme();
	}
}

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

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

void writeLangPack() {
	auto langpack = Lang::Current().serialize();
	if (!_langPackKey) {
		_langPackKey = genKey(FileOption::Safe);
		writeSettings();
	}

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

	FileWriteDescriptor file(_langPackKey, FileOption::Safe);
	file.writeEncrypted(data, SettingsKey);
}

void saveRecentLanguages(const std::vector<Lang::Language> &list) {
	if (list.empty()) {
		if (_languagesKey) {
			clearKey(_languagesKey, FileOption::Safe);
			_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 = genKey(FileOption::Safe);
		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, FileOption::Safe);
	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, FileOption::Safe, 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;
}

bool copyThemeColorsToPalette(const QString &path) {
	auto &themeKey = Window::Theme::IsNightMode()
		? _themeKeyNight
		: _themeKeyDay;
	if (!themeKey) {
		return false;
	}

	FileReadDescriptor theme;
	if (!readEncryptedFile(theme, themeKey, FileOption::Safe, SettingsKey)) {
		return false;
	}

	QByteArray themeContent;
	theme.stream >> themeContent;
	if (theme.stream.status() != QDataStream::Ok) {
		return false;
	}

	return Window::Theme::CopyColorsToPalette(path, themeContent);
}

void writeRecentHashtagsAndBots() {
	if (!_working()) return;

	const RecentHashtagPack &write(cRecentWriteHashtags()), &search(cRecentSearchHashtags());
	const RecentInlineBots &bots(cRecentInlineBots());
	if (write.isEmpty() && search.isEmpty() && bots.isEmpty()) readRecentHashtagsAndBots();
	if (write.isEmpty() && search.isEmpty() && bots.isEmpty()) {
		if (_recentHashtagsAndBotsKey) {
			clearKey(_recentHashtagsAndBotsKey);
			_recentHashtagsAndBotsKey = 0;
			_mapChanged = true;
		}
		_writeMap();
	} else {
		if (!_recentHashtagsAndBotsKey) {
			_recentHashtagsAndBotsKey = genKey();
			_mapChanged = true;
			_writeMap(WriteMapWhen::Fast);
		}
		quint32 size = sizeof(quint32) * 3, writeCnt = 0, searchCnt = 0, botsCnt = cRecentInlineBots().size();
		for (auto i = write.cbegin(), e = write.cend(); i != e;  ++i) {
			if (!i->first.isEmpty()) {
				size += Serialize::stringSize(i->first) + sizeof(quint16);
				++writeCnt;
			}
		}
		for (auto i = search.cbegin(), e = search.cend(); i != e; ++i) {
			if (!i->first.isEmpty()) {
				size += Serialize::stringSize(i->first) + sizeof(quint16);
				++searchCnt;
			}
		}
		for (auto i = bots.cbegin(), e = bots.cend(); i != e; ++i) {
			size += Serialize::peerSize(*i);
		}

		EncryptedDescriptor data(size);
		data.stream << quint32(writeCnt) << quint32(searchCnt);
		for (auto i = write.cbegin(), e = write.cend(); i != e; ++i) {
			if (!i->first.isEmpty()) data.stream << i->first << quint16(i->second);
		}
		for (auto i = search.cbegin(), e = search.cend(); i != e; ++i) {
			if (!i->first.isEmpty()) data.stream << i->first << quint16(i->second);
		}
		data.stream << quint32(botsCnt);
		for (auto i = bots.cbegin(), e = bots.cend(); i != e; ++i) {
			Serialize::writePeer(data.stream, *i);
		}
		FileWriteDescriptor file(_recentHashtagsAndBotsKey);
		file.writeEncrypted(data);
	}
}

void readRecentHashtagsAndBots() {
	if (_recentHashtagsAndBotsWereRead) return;
	_recentHashtagsAndBotsWereRead = true;

	if (!_recentHashtagsAndBotsKey) return;

	FileReadDescriptor hashtags;
	if (!readEncryptedFile(hashtags, _recentHashtagsAndBotsKey)) {
		clearKey(_recentHashtagsAndBotsKey);
		_recentHashtagsAndBotsKey = 0;
		_writeMap();
		return;
	}

	quint32 writeCount = 0, searchCount = 0, botsCount = 0;
	hashtags.stream >> writeCount >> searchCount;

	QString tag;
	quint16 count;

	RecentHashtagPack write, search;
	RecentInlineBots bots;
	if (writeCount) {
		write.reserve(writeCount);
		for (uint32 i = 0; i < writeCount; ++i) {
			hashtags.stream >> tag >> count;
			write.push_back(qMakePair(tag.trimmed(), count));
		}
	}
	if (searchCount) {
		search.reserve(searchCount);
		for (uint32 i = 0; i < searchCount; ++i) {
			hashtags.stream >> tag >> count;
			search.push_back(qMakePair(tag.trimmed(), count));
		}
	}
	cSetRecentWriteHashtags(write);
	cSetRecentSearchHashtags(search);

	if (!hashtags.stream.atEnd()) {
		hashtags.stream >> botsCount;
		if (botsCount) {
			bots.reserve(botsCount);
			for (auto i = 0; i < botsCount; ++i) {
				const auto peer = Serialize::readPeer(
					hashtags.version,
					hashtags.stream);
				if (!peer) {
					return; // Broken data.
				} else if (peer->isUser()
					&& peer->asUser()->botInfo
					&& !peer->asUser()->botInfo->inlinePlaceholder.isEmpty()
					&& !peer->asUser()->username.isEmpty()) {
					bots.push_back(peer->asUser());
				}
			}
		}
		cSetRecentInlineBots(bots);
	}
}

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));
		}
	}
}

std::optional<RecentHashtagPack> saveRecentHashtags(
		Fn<RecentHashtagPack()> getPack,
		const QString &text) {
	auto found = false;
	auto m = QRegularExpressionMatch();
	auto recent = getPack();
	for (auto i = 0, next = 0; (m = TextUtilities::RegExpHashtag().match(text, i)).hasMatch(); i = next) {
		i = m.capturedStart();
		next = m.capturedEnd();
		if (m.hasMatch()) {
			if (!m.capturedRef(1).isEmpty()) {
				++i;
			}
			if (!m.capturedRef(2).isEmpty()) {
				--next;
			}
		}
		const auto tag = text.mid(i + 1, next - i - 1);
		if (TextUtilities::RegExpHashtagExclude().match(tag).hasMatch()) {
			continue;
		}
		if (!found
			&& cRecentWriteHashtags().isEmpty()
			&& cRecentSearchHashtags().isEmpty()) {
			Local::readRecentHashtagsAndBots();
			recent = getPack();
		}
		found = true;
		incrementRecentHashtag(recent, tag);
	}
	return found ? base::make_optional(recent) : std::nullopt;
}

void saveRecentSentHashtags(const QString &text) {
	const auto result = saveRecentHashtags(
		[] { return cRecentWriteHashtags(); },
		text);
	if (result) {
		cSetRecentWriteHashtags(*result);
		Local::writeRecentHashtagsAndBots();
	}
}

void saveRecentSearchHashtags(const QString &text) {
	const auto result = saveRecentHashtags(
		[] { return cRecentSearchHashtags(); },
		text);
	if (result) {
		cSetRecentSearchHashtags(*result);
		Local::writeRecentHashtagsAndBots();
	}
}

void WriteExportSettings(const Export::Settings &settings) {
	if (!_working()) return;

	const auto check = Export::Settings();
	if (settings.types == check.types
		&& settings.fullChats == check.fullChats
		&& settings.media.types == check.media.types
		&& settings.media.sizeLimit == check.media.sizeLimit
		&& settings.path == check.path
		&& settings.format == check.format
		&& settings.availableAt == check.availableAt
		&& !settings.onlySinglePeer()) {
		if (_exportSettingsKey) {
			clearKey(_exportSettingsKey);
			_exportSettingsKey = 0;
			_mapChanged = true;
		}
		_writeMap();
	} else {
		if (!_exportSettingsKey) {
			_exportSettingsKey = genKey();
			_mapChanged = true;
			_writeMap(WriteMapWhen::Fast);
		}
		quint32 size = sizeof(quint32) * 6
			+ Serialize::stringSize(settings.path)
			+ sizeof(qint32) * 2 + sizeof(quint64);
		EncryptedDescriptor data(size);
		data.stream
			<< quint32(settings.types)
			<< quint32(settings.fullChats)
			<< quint32(settings.media.types)
			<< quint32(settings.media.sizeLimit)
			<< quint32(settings.format)
			<< settings.path
			<< quint32(settings.availableAt);
		settings.singlePeer.match([&](const MTPDinputPeerUser &user) {
			data.stream
				<< kSinglePeerTypeUser
				<< qint32(user.vuser_id.v)
				<< quint64(user.vaccess_hash.v);
		}, [&](const MTPDinputPeerChat &chat) {
			data.stream << kSinglePeerTypeChat << qint32(chat.vchat_id.v);
		}, [&](const MTPDinputPeerChannel &channel) {
			data.stream
				<< kSinglePeerTypeChannel
				<< qint32(channel.vchannel_id.v)
				<< quint64(channel.vaccess_hash.v);
		}, [&](const MTPDinputPeerSelf &) {
			data.stream << kSinglePeerTypeSelf;
		}, [&](const MTPDinputPeerEmpty &) {
			data.stream << kSinglePeerTypeEmpty;
		});
		data.stream << qint32(settings.singlePeerFrom);
		data.stream << qint32(settings.singlePeerTill);

		FileWriteDescriptor file(_exportSettingsKey);
		file.writeEncrypted(data);
	}
}

Export::Settings ReadExportSettings() {
	FileReadDescriptor file;
	if (!readEncryptedFile(file, _exportSettingsKey)) {
		clearKey(_exportSettingsKey);
		_exportSettingsKey = 0;
		_writeMap();
		return Export::Settings();
	}

	quint32 types = 0, fullChats = 0;
	quint32 mediaTypes = 0, mediaSizeLimit = 0;
	quint32 format = 0, availableAt = 0;
	QString path;
	qint32 singlePeerType = 0, singlePeerBareId = 0;
	quint64 singlePeerAccessHash = 0;
	qint32 singlePeerFrom = 0, singlePeerTill = 0;
	file.stream
		>> types
		>> fullChats
		>> mediaTypes
		>> mediaSizeLimit
		>> format
		>> path
		>> availableAt;
	if (!file.stream.atEnd()) {
		file.stream >> singlePeerType;
		switch (singlePeerType) {
		case kSinglePeerTypeUser:
		case kSinglePeerTypeChannel: {
			file.stream >> singlePeerBareId >> singlePeerAccessHash;
		} break;
		case kSinglePeerTypeChat: file.stream >> singlePeerBareId; break;
		case kSinglePeerTypeSelf:
		case kSinglePeerTypeEmpty: break;
		default: return Export::Settings();
		}
	}
	if (!file.stream.atEnd()) {
		file.stream >> singlePeerFrom >> singlePeerTill;
	}
	auto result = Export::Settings();
	result.types = Export::Settings::Types::from_raw(types);
	result.fullChats = Export::Settings::Types::from_raw(fullChats);
	result.media.types = Export::MediaSettings::Types::from_raw(mediaTypes);
	result.media.sizeLimit = mediaSizeLimit;
	result.format = Export::Output::Format(format);
	result.path = path;
	result.availableAt = availableAt;
	result.singlePeer = [&] {
		switch (singlePeerType) {
		case kSinglePeerTypeUser:
			return MTP_inputPeerUser(
				MTP_int(singlePeerBareId),
				MTP_long(singlePeerAccessHash));
		case kSinglePeerTypeChat:
			return MTP_inputPeerChat(MTP_int(singlePeerBareId));
		case kSinglePeerTypeChannel:
			return MTP_inputPeerChannel(
				MTP_int(singlePeerBareId),
				MTP_long(singlePeerAccessHash));
		case kSinglePeerTypeSelf:
			return MTP_inputPeerSelf();
		case kSinglePeerTypeEmpty:
			return MTP_inputPeerEmpty();
		}
		Unexpected("Type in export data single peer.");
	}();
	result.singlePeerFrom = singlePeerFrom;
	result.singlePeerTill = singlePeerTill;
	return (file.stream.status() == QDataStream::Ok && result.validate())
		? result
		: Export::Settings();
}

void writeSavedPeers() {
	if (!_working()) return;

	const SavedPeers &saved(cSavedPeers());
	if (saved.isEmpty()) {
		if (_savedPeersKey) {
			clearKey(_savedPeersKey);
			_savedPeersKey = 0;
			_mapChanged = true;
		}
		_writeMap();
	} else {
		if (!_savedPeersKey) {
			_savedPeersKey = genKey();
			_mapChanged = true;
			_writeMap(WriteMapWhen::Fast);
		}
		quint32 size = sizeof(quint32);
		for (SavedPeers::const_iterator i = saved.cbegin(); i != saved.cend(); ++i) {
			size += Serialize::peerSize(i.key()) + Serialize::dateTimeSize();
		}

		EncryptedDescriptor data(size);
		data.stream << quint32(saved.size());
		for (SavedPeers::const_iterator i = saved.cbegin(); i != saved.cend(); ++i) {
			Serialize::writePeer(data.stream, i.key());
			data.stream << i.value();
		}

		FileWriteDescriptor file(_savedPeersKey);
		file.writeEncrypted(data);
	}
}

void readSavedPeers() {
	if (!_savedPeersKey) return;

	FileReadDescriptor saved;
	if (!readEncryptedFile(saved, _savedPeersKey)) {
		clearKey(_savedPeersKey);
		_savedPeersKey = 0;
		_writeMap();
		return;
	}
	if (saved.version == 9011) { // broken dev version
		clearKey(_savedPeersKey);
		_savedPeersKey = 0;
		_writeMap();
		return;
	}

	quint32 count = 0;
	saved.stream >> count;
	cRefSavedPeers().clear();
	cRefSavedPeersByTime().clear();
	QList<PeerData*> peers;
	peers.reserve(count);
	for (uint32 i = 0; i < count; ++i) {
		const auto peer = Serialize::readPeer(saved.version, saved.stream);
		if (!peer) break;

		QDateTime t;
		saved.stream >> t;

		cRefSavedPeers().insert(peer, t);
		cRefSavedPeersByTime().insert(t, peer);
		peers.push_back(peer);
	}

	Auth().api().requestPeers(peers);
}

void addSavedPeer(PeerData *peer, const QDateTime &position) {
	auto &savedPeers = cRefSavedPeers();
	auto i = savedPeers.find(peer);
	if (i == savedPeers.cend()) {
		savedPeers.insert(peer, position);
	} else if (i.value() != position) {
		cRefSavedPeersByTime().remove(i.value(), peer);
		i.value() = position;
		cRefSavedPeersByTime().insert(i.value(), peer);
	}
	writeSavedPeers();
}

void removeSavedPeer(PeerData *peer) {
	auto &savedPeers = cRefSavedPeers();
	if (savedPeers.isEmpty()) return;

	auto i = savedPeers.find(peer);
	if (i != savedPeers.cend()) {
		cRefSavedPeersByTime().remove(i.value(), peer);
		savedPeers.erase(i);

		writeSavedPeers();
	}
}

void writeReportSpamStatuses() {
	_writeReportSpamStatuses();
}

void writeSelf() {
	_mapChanged = true;
	_writeMap();
}

void readSelf(const QByteArray &serialized, int32 streamVersion) {
	QDataStream stream(serialized);
	const auto user = Auth().user();
	const auto wasLoadedStatus = std::exchange(
		user->loadedStatus,
		PeerData::NotLoaded);
	const auto self = Serialize::readPeer(streamVersion, stream);
	if (!self || !self->isSelf() || self != user) {
		user->loadedStatus = wasLoadedStatus;
		return;
	}

	QString about;
	stream >> about;
	if (_checkStreamStatus(stream)) {
		self->asUser()->setAbout(about);
	}
}

void writeTrustedBots() {
	if (!_working()) return;

	if (_trustedBots.isEmpty()) {
		if (_trustedBotsKey) {
			clearKey(_trustedBotsKey);
			_trustedBotsKey = 0;
			_mapChanged = true;
			_writeMap();
		}
	} else {
		if (!_trustedBotsKey) {
			_trustedBotsKey = genKey();
			_mapChanged = true;
			_writeMap(WriteMapWhen::Fast);
		}
		quint32 size = sizeof(qint32) + _trustedBots.size() * sizeof(quint64);
		EncryptedDescriptor data(size);
		data.stream << qint32(_trustedBots.size());
		for_const (auto botId, _trustedBots) {
			data.stream << quint64(botId);
		}

		FileWriteDescriptor file(_trustedBotsKey);
		file.writeEncrypted(data);
	}
}

void readTrustedBots() {
	if (!_trustedBotsKey) return;

	FileReadDescriptor trusted;
	if (!readEncryptedFile(trusted, _trustedBotsKey)) {
		clearKey(_trustedBotsKey);
		_trustedBotsKey = 0;
		_writeMap();
		return;
	}

	qint32 size = 0;
	trusted.stream >> size;
	for (int i = 0; i < size; ++i) {
		quint64 botId = 0;
		trusted.stream >> botId;
		_trustedBots.insert(botId);
	}
}

void makeBotTrusted(UserData *bot) {
	if (!isBotTrusted(bot)) {
		_trustedBots.insert(bot->id);
		writeTrustedBots();
	}
}

bool isBotTrusted(UserData *bot) {
	if (!_trustedBotsRead) {
		readTrustedBots();
		_trustedBotsRead = true;
	}
	return _trustedBots.contains(bot->id);
}

bool encrypt(const void *src, void *dst, uint32 len, const void *key128) {
	if (!LocalKey) {
		return false;
	}
	MTP::aesEncryptLocal(src, dst, len, LocalKey, key128);
	return true;
}

bool decrypt(const void *src, void *dst, uint32 len, const void *key128) {
	if (!LocalKey) {
		return false;
	}
	MTP::aesDecryptLocal(src, dst, len, LocalKey, key128);
	return true;
}

struct ClearManagerData {
	QThread *thread;
	QMutex mutex;
	QList<int> tasks;
	bool working;
};

ClearManager::ClearManager() : data(new ClearManagerData()) {
	data->thread = new QThread();
	data->working = true;
}

bool ClearManager::addTask(int task) {
	QMutexLocker lock(&data->mutex);
	if (!data->working) return false;

	if (!data->tasks.isEmpty() && (data->tasks.at(0) == ClearManagerAll)) return true;
	if (task == ClearManagerAll) {
		data->tasks.clear();
		if (!_draftsMap.isEmpty()) {
			_draftsMap.clear();
			_mapChanged = true;
		}
		if (!_draftCursorsMap.isEmpty()) {
			_draftCursorsMap.clear();
			_mapChanged = true;
		}
		if (_locationsKey) {
			_locationsKey = 0;
			_mapChanged = true;
		}
		if (_reportSpamStatusesKey) {
			_reportSpamStatusesKey = 0;
			_mapChanged = true;
		}
		if (_trustedBotsKey) {
			_trustedBotsKey = 0;
			_mapChanged = true;
		}
		if (_recentStickersKeyOld) {
			_recentStickersKeyOld = 0;
			_mapChanged = true;
		}
		if (_installedStickersKey || _featuredStickersKey || _recentStickersKey || _archivedStickersKey) {
			_installedStickersKey = _featuredStickersKey = _recentStickersKey = _archivedStickersKey = 0;
			_mapChanged = true;
		}
		if (_recentHashtagsAndBotsKey) {
			_recentHashtagsAndBotsKey = 0;
			_mapChanged = true;
		}
		if (_savedPeersKey) {
			_savedPeersKey = 0;
			_mapChanged = true;
		}
		_writeMap();
	} else {
		for (int32 i = 0, l = data->tasks.size(); i < l; ++i) {
			if (data->tasks.at(i) == task) return true;
		}
	}
	data->tasks.push_back(task);
	return true;
}

bool ClearManager::hasTask(ClearManagerTask task) {
	QMutexLocker lock(&data->mutex);
	if (data->tasks.isEmpty()) return false;
	if (data->tasks.at(0) == ClearManagerAll) return true;
	for (int32 i = 0, l = data->tasks.size(); i < l; ++i) {
		if (data->tasks.at(i) == task) return true;
	}
	return false;
}

void ClearManager::start() {
	moveToThread(data->thread);
	connect(data->thread, SIGNAL(started()), this, SLOT(onStart()));
	connect(data->thread, SIGNAL(finished()), data->thread, SLOT(deleteLater()));
	connect(data->thread, SIGNAL(finished()), this, SLOT(deleteLater()));
	data->thread->start();
}

void ClearManager::stop() {
	{
		QMutexLocker lock(&data->mutex);
		data->tasks.clear();
	}
	auto thread = data->thread;
	thread->quit();
	thread->wait();
}

ClearManager::~ClearManager() {
	delete data;
}

void ClearManager::onStart() {
	while (true) {
		int task = 0;
		bool result = false;
		{
			QMutexLocker lock(&data->mutex);
			if (data->tasks.isEmpty()) {
				data->working = false;
				break;
			}
			task = data->tasks.at(0);
		}
		switch (task) {
		case ClearManagerAll: {
			result = QDir(cTempDir()).removeRecursively();
			QDirIterator di(_userBasePath, QDir::AllEntries | QDir::Hidden | QDir::System | QDir::NoDotAndDotDot);
			while (di.hasNext()) {
				di.next();
				const QFileInfo& fi = di.fileInfo();
				if (fi.isDir() && !fi.isSymLink()) {
					if (!QDir(di.filePath()).removeRecursively()) result = false;
				} else {
					QString path = di.filePath();
					if (!path.endsWith(qstr("map0")) && !path.endsWith(qstr("map1"))) {
						if (!QFile::remove(di.filePath())) result = false;
					}
				}
			}
		} break;
		case ClearManagerDownloads:
			result = QDir(cTempDir()).removeRecursively();
		break;
		case ClearManagerStorage:
			result = true;
		break;
		}
		{
			QMutexLocker lock(&data->mutex);
			if (!data->tasks.isEmpty() && data->tasks.at(0) == task) {
				data->tasks.pop_front();
			}
			if (data->tasks.isEmpty()) {
				data->working = false;
			}
			if (result) {
				emit succeed(task, data->working ? 0 : this);
			} else {
				emit failed(task, data->working ? 0 : this);
			}
			if (!data->working) break;
		}
	}
}

namespace internal {

Manager::Manager() {
	_mapWriteTimer.setSingleShot(true);
	connect(&_mapWriteTimer, SIGNAL(timeout()), this, SLOT(mapWriteTimeout()));
	_locationsWriteTimer.setSingleShot(true);
	connect(&_locationsWriteTimer, SIGNAL(timeout()), this, SLOT(locationsWriteTimeout()));
}

void Manager::writeMap(bool fast) {
	if (!_mapWriteTimer.isActive() || fast) {
		_mapWriteTimer.start(fast ? 1 : WriteMapTimeout);
	} else if (_mapWriteTimer.remainingTime() <= 0) {
		mapWriteTimeout();
	}
}

void Manager::writingMap() {
	_mapWriteTimer.stop();
}

void Manager::writeLocations(bool fast) {
	if (!_locationsWriteTimer.isActive() || fast) {
		_locationsWriteTimer.start(fast ? 1 : WriteMapTimeout);
	} else if (_locationsWriteTimer.remainingTime() <= 0) {
		locationsWriteTimeout();
	}
}

void Manager::writingLocations() {
	_locationsWriteTimer.stop();
}

void Manager::mapWriteTimeout() {
	_writeMap(WriteMapWhen::Now);
}

void Manager::locationsWriteTimeout() {
	_writeLocations(WriteMapWhen::Now);
}

void Manager::finish() {
	if (_mapWriteTimer.isActive()) {
		mapWriteTimeout();
	}
	if (_locationsWriteTimer.isActive()) {
		locationsWriteTimeout();
	}
}

} // namespace internal
} // namespace Local