/*

Miranda NG: the free IM client for Microsoft* Windows*

Copyright (C) 2012-20 Miranda NG team (https://miranda-ng.org)
all portions of this codebase are copyrighted to the people
listed in contributors.txt.

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
*/

#include "stdafx.h"

/////////////////////////////////////////////////////////////////////////////////////////
// constructor & destructor

CDbxMDBX::CDbxMDBX(const wchar_t *tszFileName, int iMode) :
	m_bReadOnly((iMode & DBMODE_READONLY) != 0),
	m_pwszProfileName(mir_wstrdup(tszFileName)),
	m_impl(*this)
{
	m_ccDummy.nSubs = -1;
}

CDbxMDBX::~CDbxMDBX()
{
	if (m_pWriteTran)
		mdbx_txn_commit(m_pWriteTran);

	if (m_curEventsSort)
		mdbx_cursor_close(m_curEventsSort);

	mdbx_env_close(m_env);

	if (!m_bReadOnly)
		TouchFile();

	for (auto &it : hService)
		DestroyServiceFunction(it);
	UnhookEvent(hHook);
}

/////////////////////////////////////////////////////////////////////////////////////////

BOOL CDbxMDBX::Backup(const wchar_t *pwszPath)
{
	HANDLE pFile = ::CreateFile(pwszPath, GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
	if (pFile == nullptr) {
		Netlib_Logf(0, "Backup file <%S> cannot be created", pwszPath);
		return 1;
	}

	mir_cslock lck(m_csDbAccess);

	if (m_pWriteTran) {
		mdbx_txn_commit(m_pWriteTran);
		m_pWriteTran = nullptr;
	}

	int res = mdbx_env_copy2fd(m_env, pFile, MDBX_CP_COMPACT);
	if (res != MDBX_SUCCESS) {
		Netlib_Logf(0, "CDbxMDBX::Backup: mdbx_env_copy2fd failed with error code %d", res);
LBL_Fail:
		CloseHandle(pFile);
		DeleteFileW(pwszPath);
		return res;
	}

	res = FlushFileBuffers(pFile);
	if (res == 0) {
		Netlib_Logf(0, "CDbxMDBX::Backup: FlushFileBuffers failed with error code %d", GetLastError());
		goto LBL_Fail;
	}

	CloseHandle(pFile);
	return 0;
}

/////////////////////////////////////////////////////////////////////////////////////////

size_t iDefHeaderOffset = 0;
BYTE bDefHeader[] = { 0, 0, 0, 0, 0, 0, 0, 0 };

int CDbxMDBX::Check(void)
{
	FILE *pFile = _wfopen(m_pwszProfileName, L"rb");
	if (pFile == nullptr)
		return EGROKPRF_CANTREAD;

	fseek(pFile, (LONG)iDefHeaderOffset, SEEK_SET);
	BYTE buf[_countof(bDefHeader)];
	size_t cbRead = fread(buf, 1, _countof(buf), pFile);
	fclose(pFile);
	if (cbRead != _countof(buf))
		return EGROKPRF_DAMAGED;

	return (memcmp(buf, bDefHeader, _countof(bDefHeader))) ? EGROKPRF_UNKHEADER : 0;
}

/////////////////////////////////////////////////////////////////////////////////////////

BOOL CDbxMDBX::Compact()
{
	CMStringW wszTmpFile(FORMAT, L"%s.tmp", m_pwszProfileName.get());

	mir_cslock lck(m_csDbAccess);
	int res = Backup(wszTmpFile);
	if (res)
		return res;

	mdbx_env_close(m_env);

	DeleteFileW(m_pwszProfileName);
	MoveFileW(wszTmpFile, m_pwszProfileName);

	Map();
	Load();
	return 0;
}

/////////////////////////////////////////////////////////////////////////////////////////

void CDbxMDBX::DBFlush(bool bForce)
{
	if (bForce) {
		mir_cslock lck(m_csDbAccess);

		if (m_pWriteTran) {
			mdbx_txn_commit(m_pWriteTran);
			mdbx_env_sync(m_env);

			m_pWriteTran = nullptr;
			m_dbError = mdbx_txn_begin(m_env, nullptr, MDBX_TXN_READWRITE, &m_pWriteTran);
			// FIXME: throw an exception
			_ASSERT(m_dbError == MDBX_SUCCESS);
		}
	}
	else if (m_safetyMode)
		m_impl.m_timer.Start(50);
}

/////////////////////////////////////////////////////////////////////////////////////////

int CDbxMDBX::Load()
{
	MDBX_db_flags_t defFlags = MDBX_CREATE;

	m_pWriteTran = nullptr;
	m_dbError = mdbx_txn_begin(m_env, nullptr, (m_bReadOnly) ? MDBX_TXN_RDONLY : MDBX_TXN_READWRITE, &m_pWriteTran);
	if (m_pWriteTran == nullptr) {
		if (m_dbError == MDBX_TXN_FULL) {
			if (IDOK == MessageBox(NULL, TranslateT("Your database is in the obsolete format. Click OK to read the upgrade instructions or Cancel to exit"), TranslateT("Error"), MB_ICONERROR | MB_OKCANCEL))
				Utils_OpenUrl("https://www.miranda-ng.org/news/unknown-profile-format");
			return EGROKPRF_OBSOLETE;
		}
		return EGROKPRF_DAMAGED;
	}

	mdbx_dbi_open(m_pWriteTran, "global", defFlags | MDBX_INTEGERKEY, &m_dbGlobal);
	mdbx_dbi_open(m_pWriteTran, "crypto", defFlags, &m_dbCrypto);
	mdbx_dbi_open(m_pWriteTran, "contacts", defFlags | MDBX_INTEGERKEY, &m_dbContacts);
	mdbx_dbi_open(m_pWriteTran, "modules", defFlags | MDBX_INTEGERKEY, &m_dbModules);
	mdbx_dbi_open(m_pWriteTran, "events", defFlags | MDBX_INTEGERKEY, &m_dbEvents);

	mdbx_dbi_open_ex(m_pWriteTran, "eventids", defFlags, &m_dbEventIds, DBEventIdKey::Compare, nullptr);
	mdbx_dbi_open_ex(m_pWriteTran, "eventsrt", defFlags, &m_dbEventsSort, DBEventSortingKey::Compare, nullptr);
	mdbx_dbi_open_ex(m_pWriteTran, "settings", defFlags, &m_dbSettings, DBSettingKey::Compare, nullptr);

	uint32_t keyVal = 1;
	{
		MDBX_val key = { &keyVal, sizeof(keyVal) }, data;
		if (mdbx_get(m_pWriteTran, m_dbGlobal, &key, &data) == MDBX_SUCCESS) {
			const DBHeader *hdr = (const DBHeader *)data.iov_base;
			if (hdr->dwSignature != DBHEADER_SIGNATURE)
				return EGROKPRF_DAMAGED;
			if (hdr->dwVersion != DBHEADER_VERSION)
				return EGROKPRF_OBSOLETE;

			m_header = *hdr;
		} else {
			m_header.dwSignature = DBHEADER_SIGNATURE;
			m_header.dwVersion = DBHEADER_VERSION;
			data.iov_base = &m_header; data.iov_len = sizeof(m_header);
			mdbx_put(m_pWriteTran, m_dbGlobal, &key, &data, MDBX_UPSERT);
			DBFlush();
		}

		keyVal = 2;
		if (mdbx_get(m_pWriteTran, m_dbGlobal, &key, &data) == MDBX_SUCCESS)
			m_ccDummy.dbc = *(const DBContact *)data.iov_base;
	}

	m_curEventsSort = mdbx_cursor_create(nullptr);

	MDBX_val key, val;
	{
		cursor_ptr pCursor(m_pWriteTran, m_dbEvents);
		if (mdbx_cursor_get(pCursor, &key, &val, MDBX_LAST) == MDBX_SUCCESS)
			m_dwMaxEventId = *(MEVENT *)key.iov_base;
	}
	{
		cursor_ptr pCursor(m_pWriteTran, m_dbContacts);
		if (mdbx_cursor_get(pCursor, &key, &val, MDBX_LAST) == MDBX_SUCCESS)
			m_maxContactId = *(MCONTACT *)key.iov_base;
	}

	mdbx_txn_commit(m_pWriteTran); m_pWriteTran = nullptr;

	if (InitModules()) return EGROKPRF_DAMAGED;
	if (InitCrypt())   return EGROKPRF_DAMAGED;

	FillContacts();
	FillSettings();

	return EGROKPRF_NOERROR;
}

/////////////////////////////////////////////////////////////////////////////////////////

static void assert_func(const MDBX_env*, const char *msg, const char *function, unsigned line) MDBX_CXX17_NOEXCEPT
{
	Netlib_Logf(nullptr, "MDBX: assertion failed (%s, %d): %s", function, line, msg);

	#if defined(_DEBUG)
		_wassert(_A2T(msg).get(), _A2T(function).get(), line);
	#endif
}

int CDbxMDBX::Map()
{
	if (!LockName(m_pwszProfileName))
		return EGROKPRF_CANTREAD;

	mdbx_env_create(&m_env);
	mdbx_env_set_maxdbs(m_env, 10);
	mdbx_env_set_userctx(m_env, this);
	mdbx_env_set_assert(m_env, assert_func);

#ifdef _WIN64
	mdbx_env_set_maxreaders(m_env, 1024);
	__int64 upperLimit = 0x400000000ul;
#else
	mdbx_env_set_maxreaders(m_env, 244);
	intptr_t upperLimit = 512ul << 20;
#endif

	int rc = mdbx_env_set_geometry(m_env,
		-1,   // minimal lower limit
		1ul << 20,   // at least 1M for now
		upperLimit,	// 512M upper size
		1ul << 20,   // 1M growth step
		512ul << 10,   // 512K shrink threshold
		-1);  // default page size
	if (rc != MDBX_SUCCESS)
		return EGROKPRF_CANTREAD;

	MDBX_env_flags_t mode = MDBX_NOSUBDIR | MDBX_NOTLS | MDBX_SAFE_NOSYNC | MDBX_COALESCE | MDBX_EXCLUSIVE;
	if (m_bReadOnly)
		mode |= MDBX_RDONLY;

	if (mdbx_env_open(m_env, _T2A(m_pwszProfileName), mode, 0664) != MDBX_SUCCESS)
		return EGROKPRF_CANTREAD;

	return EGROKPRF_NOERROR;
}

/////////////////////////////////////////////////////////////////////////////////////////

int CDbxMDBX::PrepareCheck()
{
	InitModules();
	return InitCrypt();
}

/////////////////////////////////////////////////////////////////////////////////////////

void CDbxMDBX::SetCacheSafetyMode(BOOL bIsSet)
{
	m_safetyMode = bIsSet != 0;
	DBFlush(true);
}

/////////////////////////////////////////////////////////////////////////////////////////

MDBX_txn* CDbxMDBX::StartTran()
{
	mir_cslock lck(m_csDbAccess);

	if (m_pWriteTran == nullptr) {
		m_dbError = mdbx_txn_begin(m_env, nullptr, (m_bReadOnly) ? MDBX_TXN_RDONLY : MDBX_TXN_READWRITE, &m_pWriteTran);
		// FIXME: throw an exception
		_ASSERT(m_dbError == MDBX_SUCCESS);
	}

	return m_pWriteTran;
}

/////////////////////////////////////////////////////////////////////////////////////////

void CDbxMDBX::TouchFile()
{
	SYSTEMTIME st;
	::GetSystemTime(&st);

	FILETIME ft;
	SystemTimeToFileTime(&st, &ft);

	HANDLE hFile = CreateFileW(m_pwszProfileName, GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
	if (hFile != INVALID_HANDLE_VALUE) {
		SetFileTime(hFile, nullptr, &ft, &ft);
		CloseHandle(hFile);
	}
}

///////////////////////////////////////////////////////////////////////////////
// MIDatabaseChecker

typedef int (CDbxMDBX::*CheckWorker)(void);

int CDbxMDBX::Start(DBCHeckCallback *callback)
{
	cb = callback;
	return ERROR_SUCCESS;
}

static CheckWorker Workers[] =
{
	&CDbxMDBX::CheckEvents1,
	&CDbxMDBX::CheckEvents2,
	&CDbxMDBX::CheckEvents3,
};

int CDbxMDBX::CheckDb(int phase)
{
	if (phase >= _countof(Workers))
		return ERROR_OUT_OF_PAPER;

	return (this->*Workers[phase])();
}

void CDbxMDBX::Destroy()
{
}