/**********************************************************************
 * LeechCraft - modular cross-platform feature rich internet client.
 * Copyright (C) 2006-2014  Georg Rudoy
 *
 * Boost Software License - Version 1.0 - August 17th, 2003
 *
 * Permission is hereby granted, free of charge, to any person or organization
 * obtaining a copy of the software and accompanying documentation covered by
 * this license (the "Software") to use, reproduce, display, distribute,
 * execute, and transmit the Software, and to prepare derivative works of the
 * Software, and to permit third-parties to whom the Software is furnished to
 * do so, all subject to the following:
 *
 * The copyright notices in the Software and this entire statement, including
 * the above license grant, this restriction and the following disclaimer,
 * must be included in all copies of the Software, in whole or in part, and
 * all derivative works of the Software, unless such copies or derivative
 * works are solely in the form of machine-executable object code generated by
 * a source language processor.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
 * SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
 * FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
 * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 **********************************************************************/

#include "storage.h"
#include <algorithm>
#include <stdexcept>
#include <QStringList>
#include <QSqlDatabase>
#include <QSqlError>
#include <QDir>
#include <QtDebug>
#include <util/db/dblock.h>
#include <util/sys/paths.h>
#include <util/sll/prelude.h>
#include <util/sll/qtutil.h>
#include <util/util.h>
#include <interfaces/azoth/iclentry.h>
#include <interfaces/azoth/iaccount.h>
#include "xmlsettingsmanager.h"

namespace LeechCraft
{
namespace Azoth
{
namespace ChatHistory
{
	Storage::RawSearchResult::RawSearchResult ()
	: EntryID_ (0)
	, AccountID_ (0)
	{
	}

	Storage::RawSearchResult::RawSearchResult (qint32 entryId, qint32 accountId, const QDateTime& date)
	: EntryID_ (entryId)
	, AccountID_ (accountId)
	, Date_ (date)
	{
	}

	bool Storage::RawSearchResult::IsEmpty () const
	{
		return Date_.isNull () || !EntryID_ || !AccountID_;
	}

	Storage::Storage (QObject *parent)
	: QObject (parent)
	{
		DB_.reset (new QSqlDatabase (QSqlDatabase::addDatabase ("QSQLITE", "History connection")));
		DB_->setDatabaseName (Util::CreateIfNotExists ("azoth").filePath ("history.db"));
		if (!DB_->open ())
		{
			qWarning () << Q_FUNC_INFO
					<< "unable to open the database";
			Util::DBLock::DumpError (DB_->lastError ());
			throw std::runtime_error ("unable to open Azoth history database");
		}

		QSqlQuery pragma (*DB_);
		pragma.exec ("PRAGMA foreign_keys = ON;");
		pragma.exec ("PRAGMA synchronous = OFF;");

		InitializeTables ();

		UserSelector_ = QSqlQuery (*DB_);
		UserSelector_.prepare ("SELECT Id, EntryID FROM azoth_users");

		AccountSelector_ = QSqlQuery (*DB_);
		AccountSelector_.prepare ("SELECT Id, AccountID FROM azoth_accounts");

		UserIDSelector_ = QSqlQuery (*DB_);
		UserIDSelector_.prepare ("SELECT Id FROM azoth_users WHERE EntryID = :entry_id");

		AccountIDSelector_ = QSqlQuery (*DB_);
		AccountIDSelector_.prepare ("SELECT Id FROM azoth_accounts WHERE AccountID = :account_id");

		UserInserter_ = QSqlQuery (*DB_);
		UserInserter_.prepare ("INSERT INTO azoth_users (EntryID) VALUES (:entry_id);");

		AccountInserter_ = QSqlQuery (*DB_);
		AccountInserter_.prepare ("INSERT INTO azoth_accounts (AccountID) VALUES (:account_id);");

		MessageDumper_ = QSqlQuery (*DB_);
		MessageDumper_.prepare ("INSERT INTO azoth_history (Id, AccountID, Date, Direction, Message, Variant, Type, RichMessage, EscapePolicy) "
				"VALUES (:id, :account_id, :date, :direction, :message, :variant, :type, :rich_message, :escape_policy);");

		UsersForAccountGetter_ = QSqlQuery (*DB_);
		UsersForAccountGetter_.prepare ("SELECT DISTINCT azoth_acc2users2.UserId, EntryID FROM azoth_users, azoth_acc2users2 "
				"WHERE azoth_acc2users2.UserId = azoth_users.Id AND azoth_acc2users2.AccountID = :account_id;");

		Date2Pos_ = QSqlQuery (*DB_);
		Date2Pos_.prepare ("SELECT COUNT(1) FROM azoth_history "
				"WHERE Id = :entry_id "
				"AND AccountID = :account_id "
				"AND Date >= :date");

		GetMonthDates_ = QSqlQuery (*DB_);
		GetMonthDates_.prepare ("SELECT Date FROM azoth_history "
				"WHERE Id = :entry_id "
				"AND AccountID = :account_id "
				"AND Date >= :lower_date "
				"AND Date <= :upper_date");

		LogsSearcher_ = QSqlQuery (*DB_);
		LogsSearcher_.prepare ("SELECT date FROM azoth_history "
				"WHERE Id = :entry_id "
				"AND AccountID = :account_id "
				"AND Date = (SELECT Date FROM azoth_history "
				"	WHERE Id = :inner_entry_id "
				"	AND AccountID = :inner_account_id "
				"	AND ((Message LIKE :text AND :insensitive) OR (Message GLOB :ctext AND :sensitive)) "
				"	ORDER BY Rowid DESC "
				"	LIMIT 1 OFFSET :offset);");

		LogsSearcherWOContact_ = QSqlQuery (*DB_);
		LogsSearcherWOContact_.prepare ("SELECT Date, Id FROM azoth_history "
				"WHERE AccountID = :account_id "
				"AND Date = (SELECT Date FROM azoth_history "
				"	WHERE AccountID = :inner_account_id "
				"	AND ((Message LIKE :text AND :insensitive) OR (Message GLOB :ctext AND :sensitive)) "
				"	ORDER BY Rowid DESC "
				"	LIMIT 1 OFFSET :offset);");

		LogsSearcherWOContactAccount_ = QSqlQuery (*DB_);
		LogsSearcherWOContactAccount_.prepare ("SELECT Date, Id, AccountID FROM azoth_history "
				"WHERE Date = (SELECT Date FROM azoth_history "
				"	WHERE ((Message LIKE :text AND :insensitive) OR (Message GLOB :ctext AND :sensitive)) "
				"	ORDER BY Rowid DESC "
				"	LIMIT 1 OFFSET :offset);");

		HistoryGetter_ = QSqlQuery (*DB_);
		HistoryGetter_.prepare ("SELECT Date, Direction, Message, Variant, Type, RichMessage, EscapePolicy "
				"FROM azoth_history "
				"WHERE Id = :entry_id "
				"AND AccountID = :account_id "
				"ORDER BY Rowid DESC LIMIT :limit OFFSET :offset;");

		HistoryClearer_ = QSqlQuery (*DB_);
		HistoryClearer_.prepare ("DELETE FROM azoth_history WHERE Id = :entry_id AND AccountID = :account_id;");

		UserClearer_ = QSqlQuery (*DB_);
		UserClearer_.prepare ("DELETE FROM azoth_users WHERE Id = :user_id;");

		EntryCacheGetter_ = QSqlQuery (*DB_);
		EntryCacheGetter_.prepare ("SELECT Id, VisibleName FROM azoth_entrycache;");

		EntryCacheSetter_ = QSqlQuery (*DB_);
		EntryCacheSetter_.prepare ("INSERT OR REPLACE INTO azoth_entrycache (Id, VisibleName) "
				"VALUES (:id, :visible_name);");

		EntryCacheClearer_ = QSqlQuery (*DB_);
		EntryCacheClearer_.prepare ("DELETE FROM azoth_entrycache WHERE Id = :user_id;");

		try
		{
			Users_ = GetUsers ();
		}
		catch (const std::exception& e)
		{
			qWarning () << Q_FUNC_INFO
					<< "unable to get saved users, we would be a bit more inefficient"
					<< e.what ();
		}

		try
		{
			Accounts_ = GetAccounts ();
		}
		catch (const std::exception& e)
		{
			qWarning () << Q_FUNC_INFO
					<< "unable to get saved accounts, we would be a bit more inefficient"
					<< e.what ();
		}

		PrepareEntryCache ();
	}

	void Storage::InitializeTables ()
	{
		Util::DBLock lock (*DB_);
		try
		{
			lock.Init ();
		}
		catch (const std::runtime_error& e)
		{
			qWarning () << Q_FUNC_INFO
					<< "error locking database for transaction:"
					<< e.what ();
			throw;
		}

		QSqlQuery query (*DB_);
		QList<QPair<QString, QString>> table2query;
		table2query.append ({
					"azoth_users",
					"CREATE TABLE azoth_users ("
						"Id INTEGER PRIMARY KEY AUTOINCREMENT, "
						"EntryID TEXT "
						");"
				});
		table2query.append ({
					"azoth_accounts",
					"CREATE TABLE azoth_accounts ("
						"Id INTEGER PRIMARY KEY AUTOINCREMENT, "
						"AccountID TEXT "
						");"
				});
		table2query.append ({
					"azoth_history",
					"CREATE TABLE azoth_history ("
						"Id INTEGER, "
						"AccountId INTEGER, "
						"Date DATETIME, "
						"Direction INTEGER, "
						"Message TEXT, "
						"Variant TEXT, "
						"Type INTEGER, "
						"RichMessage TEXT, "
						"EscapePolicy VARCHAR(3), "
						"UNIQUE (Id, AccountId, Date, Direction, Message, Variant, Type) ON CONFLICT IGNORE);"
				});
		table2query.append ({
					"azoth_entrycache",
					"CREATE TABLE azoth_entrycache ("
						"Id INTEGER UNIQUE ON CONFLICT REPLACE REFERENCES azoth_users (Id) ON DELETE CASCADE, "
						"VisibleName TEXT "
						");"
				});
		table2query.append ({
					"azoth_acc2users2",
					"CREATE TABLE azoth_acc2users2 ("
						"AccountId INTEGER REFERENCES azoth_accounts (Id) ON DELETE CASCADE, "
						"UserId INTEGER REFERENCES azoth_users (Id) ON DELETE CASCADE, "
						"UNIQUE (AccountId, UserId)"
						");"
				});

		const auto& tables = DB_->tables ();
		const bool hadAcc2User = tables.contains ("azoth_acc2users2");

		if (tables.contains ("azoth_acc2users"))
			query.exec ("DROP TABLE azoth_acc2users;");

		for (const auto& pair : table2query)
		{
			if (tables.contains (pair.first))
				continue;

			const auto& queryStr = pair.second;
			if (!query.exec (queryStr))
			{
				Util::DBLock::DumpError (query);
				throw std::runtime_error ("Unable to create tables for Azoth history");
			}
		}

		UpdateTables ();

		if (!query.exec ("CREATE INDEX IF NOT EXISTS azoth_history_id_accountid ON azoth_history (Id, AccountId);"))
		{
			Util::DBLock::DumpError (query);
			throw std::runtime_error ("Unable to index `azoth_history`.");
		}

		if (!hadAcc2User)
			regenUsersCache ();

		lock.Good ();
	}

	void Storage::UpdateTables ()
	{
		QSqlQuery query { *DB_ };

		if (!query.exec ("PRAGMA table_info (azoth_history);"))
		{
			Util::DBLock::DumpError (query);
			throw std::runtime_error ("Unable to get table information about `azoth_history`.");
		}

		QSet<QString> columns;
		while (query.next ())
			columns << query.value (1).toString ();

		if (!columns.contains ("RichMessage") &&
				!query.exec ("ALTER TABLE azoth_history ADD COLUMN RichMessage TEXT;"))
		{
			Util::DBLock::DumpError (query);
			qWarning () << Q_FUNC_INFO
					<< "existing columns:"
					<< columns;
			throw std::runtime_error ("Unable to add column `RichMessage` to `azoth_history`.");
		}

		if (!columns.contains ("EscapePolicy") &&
				!query.exec ("ALTER TABLE azoth_history ADD COLUMN EscapePolicy VARCHAR(3);"))
		{
			Util::DBLock::DumpError (query);
			qWarning () << Q_FUNC_INFO
					<< "existing columns:"
					<< columns;
			throw std::runtime_error ("Unable to add column `EscapePolicy` to `azoth_history`.");
		}
	}

	QHash<QString, qint32> Storage::GetUsers ()
	{
		if (!UserSelector_.exec ())
		{
			Util::DBLock::DumpError (UserSelector_);
			throw std::runtime_error ("Unable to perform user selection for Azoth history");
		}

		QHash<QString, qint32> result;
		while (UserSelector_.next ())
			result [UserSelector_.value (1).toString ()] =
					UserSelector_.value (0).toInt ();

		return result;
	}

	qint32 Storage::GetUserID (const QString& entryId)
	{
		UserIDSelector_.bindValue (":entry_id", entryId);
		if (!UserIDSelector_.exec ())
		{
			Util::DBLock::DumpError (UserIDSelector_);
			throw std::runtime_error ("ChatHistory::Storage::GetUserID: unable to get user's ID");
		}

		if (!UserIDSelector_.next ())
			return -1;

		qint32 result = UserIDSelector_.value (0).toInt ();
		UserIDSelector_.finish ();
		return result;
	}

	void Storage::AddUser (const QString& id, const QString& accountId)
	{
		UserInserter_.bindValue (":entry_id", id);
		if (!UserInserter_.exec ())
		{
			Util::DBLock::DumpError (UserInserter_);
			return;
		}
		UserInserter_.finish ();

		qint32 numericId = 0;
		try
		{
			numericId = GetUserID (id);
		}
		catch (const std::exception& e)
		{
			qWarning () << Q_FUNC_INFO
					<< "unable to refetch id for"
					<< id
					<< "with:"
					<< e.what ();
			return;
		}

		Users_ [id] = numericId;

		QSqlQuery acc2users (*DB_);
		acc2users.prepare ("INSERT INTO azoth_acc2users2 (AccountId, UserId) VALUES (:accId, :userId);");
		acc2users.bindValue (":accId", Accounts_ [accountId]);
		acc2users.bindValue (":userId", numericId);
		if (!acc2users.exec ())
			Util::DBLock::DumpError (UserInserter_);
	}

	void Storage::PrepareEntryCache ()
	{
		if (!EntryCacheGetter_.exec ())
		{
			Util::DBLock::DumpError (EntryCacheGetter_);
			return;
		}

		while (EntryCacheGetter_.next ())
			EntryCache_ [EntryCacheGetter_.value (0).toInt ()] = EntryCacheGetter_.value (1).toString ();

		EntryCacheGetter_.finish ();

		qDebug () << Q_FUNC_INFO << "loaded" << EntryCache_.size () << "entries";
	}

	QHash<QString, qint32> Storage::GetAccounts()
	{
		if (!AccountSelector_.exec ())
		{
			Util::DBLock::DumpError (AccountSelector_);
			throw std::runtime_error ("Unable to perform account selection for Azoth history");
		}

		QHash<QString, qint32> result;
		while (AccountSelector_.next ())
			result [AccountSelector_.value (1).toString ()] =
					AccountSelector_.value (0).toInt ();

		return result;
	}

	qint32 Storage::GetAccountID (const QString& accId)
	{
		AccountIDSelector_.bindValue (":account_id", accId);
		if (!AccountIDSelector_.exec ())
		{
			Util::DBLock::DumpError (AccountIDSelector_);
			throw std::runtime_error ("ChatHistory::Storage::GetAccountID: unable to get account ID");
		}

		if (!AccountIDSelector_.next ())
			return -1;

		qint32 result = AccountIDSelector_.value (0).toInt ();
		AccountIDSelector_.finish ();
		return result;
	}

	void Storage::AddAccount (const QString& id)
	{
		AccountInserter_.bindValue (":account_id", id);
		if (!AccountInserter_.exec ())
		{
			Util::DBLock::DumpError (AccountInserter_);
			return;
		}
		AccountInserter_.finish ();

		try
		{
			Accounts_ [id] = GetAccountID (id);
		}
		catch (const std::exception& e)
		{
			qWarning () << Q_FUNC_INFO
					<< "unable to refetch id for"
					<< id
					<< "with:"
					<< e.what ();
		}
	}

	namespace
	{
		std::shared_ptr<void> CleanupQueryGuard (QSqlQuery& query)
		{
			return std::shared_ptr<void> (nullptr, [&query] (void*) { query.finish (); });
		}
	}

	Storage::RawSearchResult Storage::Search (const QString& accountId,
			const QString& entryId, const QString& text, int shift, bool cs)
	{
		if (!Accounts_.contains (accountId))
		{
			qWarning () << Q_FUNC_INFO
					<< "Accounts_ doesn't contain"
					<< accountId
					<< "; raw contents"
					<< Accounts_;
			return RawSearchResult ();
		}
		if (!Users_.contains (entryId))
		{
			qWarning () << Q_FUNC_INFO
					<< "Users_ doesn't contain"
					<< entryId
					<< "; raw contents"
					<< Users_;
			return RawSearchResult ();
		}

		const qint32 intEntryId = Users_ [entryId];
		const qint32 intAccId = Accounts_ [accountId];
		LogsSearcher_.bindValue (":entry_id", intEntryId);
		LogsSearcher_.bindValue (":account_id", intAccId);
		LogsSearcher_.bindValue (":inner_entry_id", intEntryId);
		LogsSearcher_.bindValue (":inner_account_id", intAccId);
		LogsSearcher_.bindValue (":text", '%' + text + '%');
		LogsSearcher_.bindValue (":ctext", '*' + text + '*');
		LogsSearcher_.bindValue (":sensitive", static_cast<int> (cs));
		LogsSearcher_.bindValue (":insensitive", static_cast<int> (!cs));
		LogsSearcher_.bindValue (":offset", shift);
		if (!LogsSearcher_.exec ())
		{
			Util::DBLock::DumpError (LogsSearcher_);
			return RawSearchResult ();
		}
		auto guard = CleanupQueryGuard (LogsSearcher_);

		if (!LogsSearcher_.next ())
		{
			qWarning () << Q_FUNC_INFO
					<< "unable to move to the next entry";
			return RawSearchResult ();
		}

		return RawSearchResult (intEntryId, intAccId, LogsSearcher_.value (0).toDateTime ());
	}

	Storage::RawSearchResult Storage::Search (const QString& accountId,
			const QString& text, int shift, bool cs)
	{
		if (!Accounts_.contains (accountId))
		{
			qWarning () << Q_FUNC_INFO
					<< "Accounts_ doesn't contain"
					<< accountId
					<< "; raw contents"
					<< Accounts_;
			return RawSearchResult ();
		}

		const qint32 intAccId = Accounts_ [accountId];
		LogsSearcherWOContact_.bindValue (":account_id", intAccId);
		LogsSearcherWOContact_.bindValue (":inner_account_id", intAccId);
		LogsSearcherWOContact_.bindValue (":text", '%' + text + '%');
		LogsSearcherWOContact_.bindValue (":ctext", '*' + text + '*');
		LogsSearcherWOContact_.bindValue (":sensitive", static_cast<int> (cs));
		LogsSearcherWOContact_.bindValue (":insensitive", static_cast<int> (!cs));
		LogsSearcherWOContact_.bindValue (":offset", shift);
		if (!LogsSearcherWOContact_.exec ())
		{
			Util::DBLock::DumpError (LogsSearcherWOContact_);
			return RawSearchResult ();
		}

		if (!LogsSearcherWOContact_.next ())
		{
			qWarning () << Q_FUNC_INFO
					<< "unable to move to the next entry";
			return RawSearchResult ();
		}

		auto guard = CleanupQueryGuard (LogsSearcherWOContact_);

		return RawSearchResult (LogsSearcherWOContact_.value (1).toInt (),
				intAccId,
				LogsSearcherWOContact_.value (0).toDateTime ());
	}

	Storage::RawSearchResult Storage::Search (const QString& text, int shift, bool cs)
	{
		LogsSearcherWOContactAccount_.bindValue (":text", '%' + text + '%');
		LogsSearcherWOContactAccount_.bindValue (":ctext", '*' + text + '*');
		LogsSearcherWOContactAccount_.bindValue (":sensitive", static_cast<int> (cs));
		LogsSearcherWOContactAccount_.bindValue (":insensitive", static_cast<int> (!cs));
		LogsSearcherWOContactAccount_.bindValue (":offset", shift);
		if (!LogsSearcherWOContactAccount_.exec ())
		{
			Util::DBLock::DumpError (LogsSearcherWOContactAccount_);
			return RawSearchResult ();
		}

		if (!LogsSearcherWOContactAccount_.next ())
		{
			qWarning () << Q_FUNC_INFO
					<< "unable to move to the next entry";
			return RawSearchResult ();
		}

		auto guard = CleanupQueryGuard (LogsSearcherWOContactAccount_);

		return RawSearchResult (LogsSearcherWOContactAccount_.value (1).toInt (),
				LogsSearcherWOContactAccount_.value (2).toInt (),
				LogsSearcherWOContactAccount_.value (0).toDateTime ());
	}

	void Storage::SearchDate (qint32 accountId, qint32 entryId, const QDateTime& dt)
	{
		Date2Pos_.bindValue (":date", dt);
		Date2Pos_.bindValue (":account_id", accountId);
		Date2Pos_.bindValue (":entry_id", entryId);
		if (!Date2Pos_.exec ())
		{
			Util::DBLock::DumpError (Date2Pos_);
			return;
		}

		if (!Date2Pos_.next ())
		{
			qWarning () << Q_FUNC_INFO
					<< "unable to navigate to next record";
			return;
		}

		const int index = Date2Pos_.value (0).toInt ();
		Date2Pos_.finish ();

		emit gotSearchPosition (Accounts_.key (accountId), Users_.key (entryId), index);
	}

	void Storage::regenUsersCache ()
	{
		QSqlQuery query (*DB_);
		if (!query.exec ("DELETE FROM azoth_acc2users2;") ||
			!query.exec ("INSERT INTO azoth_acc2users2 (AccountId, UserId) SELECT DISTINCT AccountId, Id FROM azoth_history;"))
		{
			Util::DBLock::DumpError (query);
			query.exec ("DROP TABLE azoth_acc2users2");
		}
	}

	void Storage::addMessage (const QVariantMap& data)
	{
		Util::DBLock lock (*DB_);
		try
		{
			lock.Init ();
		}
		catch (const std::exception& e)
		{
			qWarning () << Q_FUNC_INFO
					<< "unable to start transaction:"
					<< e.what ();
			return;
		}

		const QString& accountID = data ["AccountID"].toString ();
		if (!Accounts_.contains (accountID))
		{
			try
			{
				AddAccount (accountID);
			}
			catch (const std::exception& e)
			{
				qWarning () << Q_FUNC_INFO
						<< accountID
						<< "unable to add account ID to the DB:"
						<< e.what ();
				return;
			}
		}

		const QString& entryID = data ["EntryID"].toString ();
		if (!Users_.contains (entryID))
		{
			try
			{
				AddUser (entryID, accountID);
			}
			catch (const std::exception& e)
			{
				qWarning () << Q_FUNC_INFO
						<< entryID
						<< "unable to add the user to the DB:"
						<< e.what ();
				return;
			}
		}

		auto userId = Users_ [entryID];
		if (!EntryCache_.contains (userId))
		{
			EntryCacheSetter_.bindValue (":id", userId);
			EntryCacheSetter_.bindValue (":visible_name", data ["VisibleName"]);
			if (!EntryCacheSetter_.exec ())
				Util::DBLock::DumpError (EntryCacheSetter_);

			EntryCache_ [userId] = data ["VisibleName"].toString ();
		}

		MessageDumper_.bindValue (":id", userId);
		MessageDumper_.bindValue (":account_id", Accounts_ [accountID]);
		MessageDumper_.bindValue (":date", data ["DateTime"]);
		MessageDumper_.bindValue (":direction", data ["Direction"]);
		MessageDumper_.bindValue (":message", data ["Body"]);
		MessageDumper_.bindValue (":variant", data ["OtherVariant"]);
		MessageDumper_.bindValue (":rich_message", data ["RichBody"]);
		MessageDumper_.bindValue (":escape_policy", data ["EscapePolicy"]);

		switch (static_cast<IMessage::Type> (data ["Type"].toInt ()))
		{
		case IMessage::Type::ChatMessage:
			MessageDumper_.bindValue (":type", "CHAT");
			break;
		case IMessage::Type::MUCMessage:
			MessageDumper_.bindValue (":type", "MUC");
			break;
		case IMessage::Type::StatusMessage:
			MessageDumper_.bindValue (":type", "STATUS");
			break;
		case IMessage::Type::EventMessage:
			MessageDumper_.bindValue (":type", "EVENT");
			break;
		case IMessage::Type::ServiceMessage:
			MessageDumper_.bindValue (":type", "SERVICE");
			break;
		}

		if (!MessageDumper_.exec ())
		{
			Util::DBLock::DumpError (MessageDumper_);
			return;
		}

		lock.Good ();
	}

	void Storage::getOurAccounts ()
	{
		emit gotOurAccounts (Accounts_.keys ());
	}

	void Storage::getUsersForAccount (const QString& accountId)
	{
		if (!Accounts_.contains (accountId))
		{
			qWarning () << Q_FUNC_INFO
					<< "Accounts_ doesn't contain"
					<< accountId
					<< "; raw contents:"
					<< Accounts_;
			return;
		}

		UsersForAccountGetter_.bindValue (":account_id", Accounts_ [accountId]);
		if (!UsersForAccountGetter_.exec ())
		{
			Util::DBLock::DumpError (UsersForAccountGetter_);
			return;
		}

		QStringList result;
		QStringList cachedNames;
		while (UsersForAccountGetter_.next ())
		{
			const int id = UsersForAccountGetter_.value (0).toInt ();
			result << UsersForAccountGetter_.value (1).toString ();
			cachedNames << EntryCache_.value (id);
		}

		emit gotUsersForAccount (result, accountId, cachedNames);
	}

	void Storage::getChatLogs (const QString& accountId,
			const QString& entryId, int backpages, int amount)
	{
		if (!Accounts_.contains (accountId))
		{
			qWarning () << Q_FUNC_INFO
					<< "Accounts_ doesn't contain"
					<< accountId
					<< "; raw contents"
					<< Accounts_;
			return;
		}
		if (!Users_.contains (entryId))
		{
			qWarning () << Q_FUNC_INFO
					<< "Users_ doesn't contain"
					<< entryId
					<< "; raw contents"
					<< Users_;
			return;
		}

		HistoryGetter_.bindValue (":entry_id", Users_ [entryId]);
		HistoryGetter_.bindValue (":account_id", Accounts_ [accountId]);
		HistoryGetter_.bindValue (":limit", amount);
		HistoryGetter_.bindValue (":offset", amount * backpages);

		if (!HistoryGetter_.exec ())
		{
			Util::DBLock::DumpError (HistoryGetter_);
			return;
		}

		QList<QVariant> result;
		while (HistoryGetter_.next ())
		{
			QVariantMap map;
			map ["Date"] = HistoryGetter_.value (0);
			map ["Direction"] = HistoryGetter_.value (1);
			map ["Message"] = HistoryGetter_.value (2);
			map ["Variant"] = HistoryGetter_.value (3);
			map ["Type"] = HistoryGetter_.value (4);
			map ["RichMessage"] = HistoryGetter_.value (5);
			map ["EscapePolicy"] = HistoryGetter_.value (6);
			result.prepend (map);
		}

		emit gotChatLogs (accountId, entryId, backpages, amount, result);
	}

	void Storage::search (const QString& accountId,
			const QString& entryId, const QString& text, int shift, bool cs)
	{
		RawSearchResult res;
		if (!accountId.isEmpty () && !entryId.isEmpty ())
			res = Search (accountId, entryId, text, shift, cs);
		else if (!accountId.isEmpty ())
			res = Search (accountId, text, shift, cs);
		else
			res = Search (text, shift, cs);

		if (res.Date_.isNull ())
		{
			emit gotSearchPosition (accountId, entryId, 0);
			return;
		}

		if (res.IsEmpty ())
			return;

		SearchDate (res.AccountID_, res.EntryID_, res.Date_);
	}

	void Storage::searchDate (const QString& account, const QString& entry, const QDateTime& dt)
	{
		if (!Accounts_.contains (account))
		{
			qWarning () << Q_FUNC_INFO
					<< "Accounts_ doesn't contain"
					<< account
					<< "; raw contents"
					<< Accounts_;
			return;
		}
		if (!Users_.contains (entry))
		{
			qWarning () << Q_FUNC_INFO
					<< "Users_ doesn't contain"
					<< entry
					<< "; raw contents"
					<< Users_;
			return;
		}

		const qint32 entryId = Users_ [entry];
		const qint32 accId = Accounts_ [account];
		SearchDate (accId, entryId, dt);
	}

	void Storage::getDaysForSheet (const QString& account, const QString& entry, int year, int month)
	{
		if (!Accounts_.contains (account))
		{
			qWarning () << Q_FUNC_INFO
					<< "Accounts_ doesn't contain"
					<< account
					<< "; raw contents"
					<< Accounts_;
			return;
		}
		if (!Users_.contains (entry))
		{
			qWarning () << Q_FUNC_INFO
					<< "Users_ doesn't contain"
					<< entry
					<< "; raw contents"
					<< Users_;
			return;
		}

		const QDate lowerDate (year, month, 1);
		const QDateTime lowerBound (lowerDate, QTime (0, 0, 0));
		const QDateTime upperBound (QDate (year, month, lowerDate.daysInMonth ()), QTime (23, 59, 59));

		GetMonthDates_.bindValue (":entry_id", Users_ [entry]);
		GetMonthDates_.bindValue (":account_id", Accounts_ [account]);
		GetMonthDates_.bindValue (":lower_date", lowerBound);
		GetMonthDates_.bindValue (":upper_date", upperBound);

		if (!GetMonthDates_.exec ())
		{
			Util::DBLock::DumpError (GetMonthDates_);
			return;
		}

		QList<int> result;
		while (GetMonthDates_.next ())
		{
			const auto date = GetMonthDates_.value (0).toDate ();
			const int day = date.day ();
			if (!result.contains (day))
				result << day;
		}
		std::sort (result.begin (), result.end ());
		emit gotDaysForSheet (account, entry, year, month, result);
	}

	void Storage::clearHistory (const QString& accountId, const QString& entryId)
	{
		if (!Accounts_.contains (accountId) ||
				!Users_.contains (entryId))
		{
			qWarning () << Q_FUNC_INFO
					<< "unknown entry/account combination"
					<< accountId
					<< entryId;
			return;
		}

		Util::DBLock lock (*DB_);
		lock.Init ();

		const auto userId = Users_.take (entryId);
		HistoryClearer_.bindValue (":entry_id", userId);
		HistoryClearer_.bindValue (":account_id", Accounts_ [accountId]);

		if (!HistoryClearer_.exec ())
			Util::DBLock::DumpError (HistoryClearer_);

		EntryCacheClearer_.bindValue (":user_id", userId);
		if (!EntryCacheClearer_.exec ())
			Util::DBLock::DumpError (EntryCacheClearer_);

		UserClearer_.bindValue (":user_id", userId);
		if (!UserClearer_.exec ())
			Util::DBLock::DumpError (UserClearer_);

		lock.Good ();
	}
}
}
}
