/*

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

Copyright (�) 2012-16 Miranda NG project (http://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"

CDbxMdb::CDbxMdb(const TCHAR *tszFileName, int iMode) :
	m_safetyMode(true),
	m_bReadOnly((iMode & DBMODE_READONLY) != 0),
	m_bShared((iMode & DBMODE_SHARED) != 0),
	m_lResidentSettings(50, strcmp),
	m_maxContactId(1)
{
	m_tszProfileName = mir_tstrdup(tszFileName);
	InitDbInstance(this);

	mdb_env_create(&m_pMdbEnv);
	mdb_env_set_maxdbs(m_pMdbEnv, 10);
	mdb_env_set_userctx(m_pMdbEnv, this);
//	mdb_env_set_assert(m_pMdbEnv, LMDB_FailAssert);

	m_codePage = Langpack_GetDefaultCodePage();
}

CDbxMdb::~CDbxMdb()
{
	mdb_env_close(m_pMdbEnv);

	DestroyServiceFunction(hService);
	UnhookEvent(hHook);

	if (m_crypto)
		m_crypto->destroy();

	DestroyHookableEvent(hContactDeletedEvent);
	DestroyHookableEvent(hContactAddedEvent);
	DestroyHookableEvent(hSettingChangeEvent);
	DestroyHookableEvent(hEventMarkedRead);

	DestroyHookableEvent(hEventAddedEvent);
	DestroyHookableEvent(hEventDeletedEvent);
	DestroyHookableEvent(hEventFilterAddedEvent);

	DestroyDbInstance(this);
	mir_free(m_tszProfileName);
}

int CDbxMdb::Load(bool bSkipInit)
{
	if (Map() != MDB_SUCCESS)
		return EGROKPRF_CANTREAD;

	if (!bSkipInit) {
		txn_ptr trnlck(m_pMdbEnv);

		unsigned int defFlags = MDB_CREATE;

		mdb_dbi_open(trnlck, "global", defFlags | MDB_INTEGERKEY, &m_dbGlobal);
		mdb_dbi_open(trnlck, "crypto", defFlags, &m_dbCrypto);
		mdb_dbi_open(trnlck, "contacts", defFlags | MDB_INTEGERKEY, &m_dbContacts);
		mdb_dbi_open(trnlck, "modules", defFlags | MDB_INTEGERKEY, &m_dbModules);
		mdb_dbi_open(trnlck, "events", defFlags | MDB_INTEGERKEY, &m_dbEvents);

		mdb_dbi_open(trnlck, "eventsrt", defFlags, &m_dbEventsSort);
		mdb_set_compare(trnlck, m_dbEventsSort, DBEventSortingKey::Compare);

		mdb_dbi_open(trnlck, "settings", defFlags, &m_dbSettings);
		mdb_set_compare(trnlck, m_dbSettings, DBSettingKey::Compare);

		uint32_t keyVal = 1;
		MDB_val key = { sizeof(keyVal), &keyVal }, data;
		if (mdb_get(trnlck, m_dbGlobal, &key, &data) == MDB_SUCCESS) 
		{
			const DBHeader *hdr = (const DBHeader*)data.mv_data;
			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.mv_data = &m_header; data.mv_size = sizeof(m_header);
			mdb_put(trnlck, m_dbGlobal, &key, &data, 0);

			keyVal = 0;
			DBContact dbc = { 0, 0, 0 };
			data.mv_data = &dbc; data.mv_size = sizeof(dbc);
			mdb_put(trnlck, m_dbContacts, &key, &data, 0);
		}
		trnlck.commit();

		{
			MDB_val key, val;

			mdb_txn_begin(m_pMdbEnv, nullptr, MDB_RDONLY, &m_txn);

			mdb_cursor_open(m_txn, m_dbEvents, &m_curEvents);
			if (mdb_cursor_get(m_curEvents, &key, &val, MDB_LAST) == MDB_SUCCESS)
				m_dwMaxEventId = *(MEVENT*)key.mv_data;

			mdb_cursor_open(m_txn, m_dbEventsSort, &m_curEventsSort);
			mdb_cursor_open(m_txn, m_dbSettings, &m_curSettings);
			mdb_cursor_open(m_txn, m_dbModules, &m_curModules);

			mdb_cursor_open(m_txn, m_dbContacts, &m_curContacts);
			if (mdb_cursor_get(m_curContacts, &key, &val, MDB_LAST) == MDB_SUCCESS)
				m_maxContactId = *(MCONTACT*)key.mv_data;

			MDB_stat st;
			mdb_stat(m_txn, m_dbContacts, &st);
			m_contactCount = st.ms_entries;

			mdb_txn_reset(m_txn);
		}


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

		// everything is ok, go on
		if (!m_bReadOnly) {
			// we don't need events in the service mode
			if (ServiceExists(MS_DB_SETSAFETYMODE)) {
				hContactDeletedEvent = CreateHookableEvent(ME_DB_CONTACT_DELETED);
				hContactAddedEvent = CreateHookableEvent(ME_DB_CONTACT_ADDED);
				hSettingChangeEvent = CreateHookableEvent(ME_DB_CONTACT_SETTINGCHANGED);
				hEventMarkedRead = CreateHookableEvent(ME_DB_EVENT_MARKED_READ);

				hEventAddedEvent = CreateHookableEvent(ME_DB_EVENT_ADDED);
				hEventDeletedEvent = CreateHookableEvent(ME_DB_EVENT_DELETED);
				hEventFilterAddedEvent = CreateHookableEvent(ME_DB_EVENT_FILTER_ADD);
			}
		}

		FillContacts();
	}

	return EGROKPRF_NOERROR;
}

int CDbxMdb::Create(void)
{
	return (Map() == MDB_SUCCESS) ? 0 : EGROKPRF_CANTREAD;
}

size_t iDefHeaderOffset = 16;
BYTE bDefHeader[] = { 0xDE, 0xC0, 0xEF, 0xBE };

int CDbxMdb::Check(void)
{
	FILE *pFile = _tfopen(m_tszProfileName, L"rb");
	if (pFile == nullptr)
		return EGROKPRF_CANTREAD;

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

	fclose(pFile);

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

int CDbxMdb::PrepareCheck(int*)
{
	InitModules();
	return InitCrypt();
}

STDMETHODIMP_(void) CDbxMdb::SetCacheSafetyMode(BOOL bIsSet)
{
	m_safetyMode = bIsSet != 0;
}

int CDbxMdb::Map()
{
	unsigned int mode = MDB_NOSYNC | MDB_NOSUBDIR | /*MDB_NOLOCK |*/ MDB_NOTLS | MDB_WRITEMAP;
	if (m_bReadOnly)
		mode |= MDB_RDONLY;
	return mdb_env_open(m_pMdbEnv, _T2A(m_tszProfileName), mode, 0664);
}

bool CDbxMdb::Remap()
{
	MDB_envinfo ei;
	mdb_env_info(m_pMdbEnv, &ei);
	return mdb_env_set_mapsize(m_pMdbEnv, ei.me_mapsize + 0x100000) == MDB_SUCCESS;
}


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

static DWORD DatabaseCorrupted = 0;
static const TCHAR *msg = NULL;
static DWORD dwErr = 0;
static wchar_t tszPanic[] = LPGENW("Miranda has detected corruption in your database. This corruption may be fixed by DbChecker plugin. Please download it from http://miranda-ng.org/p/DbChecker/. Miranda will now shut down.");

EXTERN_C void __cdecl dbpanic(void *)
{
	if (msg) {
		if (dwErr == ERROR_DISK_FULL)
			msg = TranslateT("Disk is full. Miranda will now shut down.");

		TCHAR err[256];
		mir_sntprintf(err, _countof(err), msg, TranslateT("Database failure. Miranda will now shut down."), dwErr);

		MessageBox(0, err, TranslateT("Database Error"), MB_SETFOREGROUND | MB_TOPMOST | MB_APPLMODAL | MB_ICONWARNING | MB_OK);
	}
	else MessageBox(0, TranslateTS(tszPanic), TranslateT("Database Panic"), MB_SETFOREGROUND | MB_TOPMOST | MB_APPLMODAL | MB_ICONWARNING | MB_OK);
	TerminateProcess(GetCurrentProcess(), 255);
}


EXTERN_C void LMDB_FailAssert(MDB_env *env, const char *text)
{
	((CDbxMdb*)mdb_env_get_userctx(env))->DatabaseCorruption(_A2T(text));
}

EXTERN_C void LMDB_Log(const char *fmt, ...)
{
	va_list args;
	va_start(args, fmt);
	CallService(MS_NETLIB_LOG, 0, (LPARAM)(CMStringA().FormatV(fmt, args)));
	va_end(args);
}

void CDbxMdb::DatabaseCorruption(const TCHAR *text)
{
	int kill = 0;

	if (DatabaseCorrupted == 0) {
		DatabaseCorrupted++;
		kill++;
		msg = text;
		dwErr = GetLastError();
	}
	else {
		/* db is already corrupted, someone else is dealing with it, wait here
		so that we don't do any more damage */
		Sleep(INFINITE);
		return;
	}

	if (kill) {
		_beginthread(dbpanic, 0, NULL);
		Sleep(INFINITE);
	}
}

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

typedef int (CDbxMdb::*CheckWorker)(int);

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

int CDbxMdb::CheckDb(int, int)
{
	return ERROR_OUT_OF_PAPER;
}

void CDbxMdb::Destroy()
{
	delete this;
}