/*

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"

class CSelectCryptoDialog : public CDlgBase
{
	CCtrlCombo m_combo;
	CCtrlData m_descr;
	CCtrlCheck m_chkTotalCrypt;
	CRYPTO_PROVIDER **m_provs;
	size_t m_provscount;
	CRYPTO_PROVIDER *m_selected;
	bool m_bTotalEncryption;

	bool OnInitDialog() override
	{
		for (size_t i = 0; i < m_provscount; i++) {
			CRYPTO_PROVIDER *prov = m_provs[i];
			m_combo.AddStringA(prov->pszName, i);
		}
		m_combo.SetCurSel(0);
		m_descr.SetText(m_provs[0]->szDescr.w);
		return true;
	}

	bool OnClose() override
	{
		m_selected = m_provs[m_combo.GetItemData(m_combo.GetCurSel())];
		m_bTotalEncryption = m_chkTotalCrypt.GetState() != 0;
		return true;
	}

	void OnComboChanged(CCtrlCombo*)
	{
		m_descr.SetText(m_provs[m_combo.GetItemData(m_combo.GetCurSel())]->szDescr.w);
	}

public:
	CSelectCryptoDialog(CRYPTO_PROVIDER **provs, size_t count) :
		CDlgBase(g_plugin, IDD_SELECT_CRYPTOPROVIDER),
		m_combo(this, IDC_SELECTCRYPT_COMBO),
		m_descr(this, IDC_CRYPTOPROVIDER_DESCR),
		m_chkTotalCrypt(this, IDC_CHECK_TOTALCRYPT),
		m_provs(provs),
		m_provscount(count),
		m_selected(nullptr)
	{
		m_combo.OnChange = Callback(this, &CSelectCryptoDialog::OnComboChanged);
	}

	inline CRYPTO_PROVIDER* GetSelected() 
	{	return m_selected;
	}
	inline bool TotalSelected()
	{	return m_bTotalEncryption;
	}
};

char DBKey_Crypto_Provider[] = "Provider";
char DBKey_Crypto_Key[] = "Key";
char DBKey_Crypto_IsEncrypted[] = "EncryptedDB";

CRYPTO_PROVIDER* CDbxMDBX::SelectProvider()
{
	CRYPTO_PROVIDER **ppProvs, *pProv;
	int iNumProvs;
	Crypto_EnumProviders(&iNumProvs, &ppProvs);

	if (iNumProvs == 0)
		return nullptr;

	bool bTotalCrypt = false;

	if (iNumProvs > 1) {
		CSelectCryptoDialog dlg(ppProvs, iNumProvs);
		dlg.DoModal();
		pProv = dlg.GetSelected();
		bTotalCrypt = dlg.TotalSelected();
	}
	else pProv = ppProvs[0];

	{
		txn_ptr trnlck(StartTran());
		MDBX_val key = { DBKey_Crypto_Provider, sizeof(DBKey_Crypto_Provider) }, value = { pProv->pszName, mir_strlen(pProv->pszName) + 1 };
		if (mdbx_put(trnlck, m_dbCrypto, &key, &value, 0) != MDBX_SUCCESS)
			return nullptr;

		key.iov_len = sizeof(DBKey_Crypto_IsEncrypted); key.iov_base = DBKey_Crypto_IsEncrypted; value.iov_len = sizeof(bool); value.iov_base = &bTotalCrypt;
		if (mdbx_put(trnlck, m_dbCrypto, &key, &value, 0) != MDBX_SUCCESS)
			return nullptr;

		if (trnlck.commit() != MDBX_SUCCESS)
			return nullptr;
	}

	DBFlush();
	return pProv;
}

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

class CEnterPasswordDialog : public CDlgBase
{
	CCtrlData m_header;
	CCtrlData m_language;
	CCtrlEdit m_passwordEdit;
	CCtrlButton m_buttonOK;

	friend class CDbxMDBX;
	CDbxMDBX *m_db;
	TCHAR m_newPass[100];
	unsigned short m_wrongPass = 0;

	INT_PTR DlgProc(UINT msg, WPARAM wParam, LPARAM lParam) override
	{
		if (msg == WM_TIMER) {
			UINT_PTR LangID = (UINT_PTR)GetKeyboardLayout(0);
			char Lang[3] = { 0 };
			GetLocaleInfoA(MAKELCID((LangID & 0xffffffff), SORT_DEFAULT), LOCALE_SABBREVLANGNAME, Lang, 2);
			Lang[0] = toupper(Lang[0]);
			Lang[1] = tolower(Lang[1]);
			m_language.SetTextA(Lang);
			return FALSE;
		}
		else if (msg == WM_CTLCOLORSTATIC) {
			if ((HWND)lParam == m_language.GetHwnd()) {
				SetTextColor((HDC)wParam, GetSysColor(COLOR_HIGHLIGHTTEXT));
				SetBkMode((HDC)wParam, TRANSPARENT);
				return (INT_PTR)GetSysColorBrush(COLOR_HIGHLIGHT);
			}
		}
		return CDlgBase::DlgProc(msg, wParam, lParam);
	}

	bool OnInitDialog() override
	{
		m_header.SendMsg(WM_SETICON, ICON_SMALL, (LPARAM)g_plugin.getIcon(IDI_LOGO, true));

		if (m_wrongPass) {
			if (m_wrongPass > 2) {
				m_passwordEdit.Disable();
				m_buttonOK.Disable();
				m_header.SetText(TranslateT("Too many errors!"));
			}
			else m_header.SetText(TranslateT("Password is not correct!"));
		}
		else m_header.SetText(TranslateT("Please type in your password"));

		SetTimer(m_hwnd, 1, 200, nullptr);
		return true;
	}

	void OnDestroy() override
	{
		KillTimer(m_hwnd, 1);
		Window_FreeIcon_IcoLib(m_header.GetHwnd());
	}

	void OnOK(CCtrlButton*)
	{
		m_passwordEdit.GetText(m_newPass, _countof(m_newPass));
		EndDialog(m_hwnd, -128);
	}

public:
	CEnterPasswordDialog(CDbxMDBX *db) :
		CDlgBase(g_plugin, IDD_LOGIN),
		m_header(this, IDC_HEADERBAR),
		m_language(this, IDC_LANG),
		m_passwordEdit(this, IDC_USERPASS),
		m_buttonOK(this, IDOK),
		m_db(db)
	{
		m_newPass[0] = 0;
		m_buttonOK.OnClick = Callback(this, &CEnterPasswordDialog::OnOK);
	}
};

int CDbxMDBX::InitCrypt()
{
	if (m_crypto != nullptr)
		return 0;

	int rc;
	MDBX_val key = { DBKey_Crypto_Provider, sizeof(DBKey_Crypto_Provider) }, value;
	{
		txn_ptr_ro txn(m_txn_ro);
		rc = mdbx_get(txn, m_dbCrypto, &key, &value);
	}

	CRYPTO_PROVIDER *pProvider;
	if (rc == MDBX_SUCCESS) {
		pProvider = Crypto_GetProvider((const char*)value.iov_base);
		if (pProvider == nullptr)
			pProvider = SelectProvider();
	}
	else pProvider = SelectProvider();

	if (pProvider == nullptr)
		return 1;

	if ((m_crypto = pProvider->pFactory()) == nullptr)
		return 3;

	key.iov_len = sizeof(DBKey_Crypto_Key); key.iov_base = DBKey_Crypto_Key;
	{
		txn_ptr_ro txn(m_txn_ro);
		rc = mdbx_get(txn, m_dbCrypto, &key, &value);
	}
	
	if (rc == MDBX_SUCCESS && (value.iov_len == m_crypto->getKeyLength())) {
		if (!m_crypto->setKey((const BYTE*)value.iov_base, value.iov_len)) {
			CEnterPasswordDialog dlg(this);
			while (true) {
				if (-128 != dlg.DoModal())
					return 4;
				m_crypto->setPassword(pass_ptrA(mir_utf8encodeW(dlg.m_newPass)));
				if (m_crypto->setKey((const BYTE*)value.iov_base, value.iov_len)) {
					m_bUsesPassword = true;
					SecureZeroMemory(&dlg.m_newPass, sizeof(dlg.m_newPass));
					break;
				}
				dlg.m_wrongPass++;
			}
		}
	}
	else {
		if (!m_crypto->generateKey())
			return 6;
		StoreKey();
	}

	key.iov_len = sizeof(DBKey_Crypto_IsEncrypted); key.iov_base = DBKey_Crypto_IsEncrypted;
	{
		txn_ptr_ro txn(m_txn_ro);
		if (mdbx_get(txn, m_dbCrypto, &key, &value) == MDBX_SUCCESS)
			m_bEncrypted = *(const bool *)value.iov_base;
		else
			m_bEncrypted = false;
	}

	InitDialogs();
	return 0;
}

void CDbxMDBX::StoreKey()
{
	size_t iKeyLength = m_crypto->getKeyLength();
	BYTE *pKey = (BYTE*)_alloca(iKeyLength);
	m_crypto->getKey(pKey, iKeyLength);
	{
		txn_ptr trnlck(StartTran());
		MDBX_val key = { DBKey_Crypto_Key, sizeof(DBKey_Crypto_Key) }, value = { pKey, iKeyLength };
		int rc = mdbx_put(trnlck, m_dbCrypto, &key, &value, 0);
		if (rc == MDBX_SUCCESS)
			rc = trnlck.commit();
		/* FIXME: throw an exception */
		assert(rc == MDBX_SUCCESS);
		UNREFERENCED_PARAMETER(rc);
	}

	SecureZeroMemory(pKey, iKeyLength);
	DBFlush();
}

void CDbxMDBX::SetPassword(const wchar_t *ptszPassword)
{
	if (ptszPassword == nullptr || *ptszPassword == 0) {
		m_bUsesPassword = false;
		m_crypto->setPassword(nullptr);
	}
	else {
		m_bUsesPassword = true;
		m_crypto->setPassword(pass_ptrA(mir_utf8encodeW(ptszPassword)));
	}
	UpdateMenuItem();
}

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

int CDbxMDBX::EnableEncryption(bool bEncrypted)
{
	if (m_bEncrypted == bEncrypted)
		return 0;

	std::vector<MEVENT> lstEvents;
	{
		txn_ptr_ro txnro(m_txn_ro);

		MDBX_stat st;
		mdbx_dbi_stat(txnro, m_dbEvents, &st, sizeof(st));

		lstEvents.reserve(st.ms_entries);
		{
			cursor_ptr_ro cursor(m_curEvents);
			MDBX_val key, data;
			while (mdbx_cursor_get(cursor, &key, &data, MDBX_NEXT) == MDBX_SUCCESS) {
				const MEVENT hDbEvent = *(const MEVENT*)key.iov_base;
				lstEvents.push_back(hDbEvent);
			}
		}
	}

	do {
		size_t portion = min(lstEvents.size(), 1000);

		txn_ptr trnlck(StartTran());
		for (size_t i = 0; i < portion; i++) {
			MEVENT &hDbEvent = lstEvents[i];
			MDBX_val key = { &hDbEvent, sizeof(MEVENT) }, data;
			int rc = mdbx_get(trnlck, m_dbEvents, &key, &data);
			if (rc != MDBX_SUCCESS) {
				if (rc != MDBX_NOTFOUND)
					assert(rc == MDBX_SUCCESS);
				continue;
			}

			const DBEvent *dbEvent = (const DBEvent*)data.iov_base;
			const BYTE    *pBlob = (BYTE*)(dbEvent + 1);

			if (((dbEvent->flags & DBEF_ENCRYPTED) != 0) != bEncrypted) {
				mir_ptr<BYTE> pNewBlob;
				size_t nNewBlob;
				uint32_t dwNewFlags;

				if (dbEvent->flags & DBEF_ENCRYPTED) {
					pNewBlob = (BYTE*)m_crypto->decodeBuffer(pBlob, dbEvent->cbBlob, &nNewBlob);
					dwNewFlags = dbEvent->flags & (~DBEF_ENCRYPTED);
				}
				else {
					pNewBlob = m_crypto->encodeBuffer(pBlob, dbEvent->cbBlob, &nNewBlob);
					dwNewFlags = dbEvent->flags | DBEF_ENCRYPTED;
				}

				data.iov_len = sizeof(DBEvent) + nNewBlob;
				mir_ptr<BYTE> pData((BYTE*)mir_alloc(data.iov_len));
				data.iov_base = pData.get();

				DBEvent *pNewDBEvent = (DBEvent *)data.iov_base;
				*pNewDBEvent = *dbEvent;
				pNewDBEvent->cbBlob = (uint16_t)nNewBlob;
				pNewDBEvent->flags = dwNewFlags;
				memcpy(pNewDBEvent + 1, pNewBlob, nNewBlob);

				if (mdbx_put(trnlck, m_dbEvents, &key, &data, 0) != MDBX_SUCCESS)
					return 1;
			}
		}

		lstEvents.erase(lstEvents.begin(), lstEvents.begin()+portion);
		if (trnlck.commit() != MDBX_SUCCESS)
			return 1;
	}
		while (lstEvents.size() > 0);

	txn_ptr trnlck(StartTran());
	MDBX_val key = { DBKey_Crypto_IsEncrypted, sizeof(DBKey_Crypto_IsEncrypted) }, value = { &bEncrypted, sizeof(bool) };
	if (mdbx_put(trnlck, m_dbCrypto, &key, &value, 0) != MDBX_SUCCESS)
		return 1;
	if (trnlck.commit() != MDBX_SUCCESS)
		return 1;

	DBFlush();
	m_bEncrypted = bEncrypted;
	return 0;
}