/*

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

Copyright (c) 2012-18 Miranda NG team (https://miranda-ng.org),
Copyright (c) 2000-12 Miranda IM project,
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"
#include "plugins.h"
#include "langpack.h"
#include "profilemanager.h"
#include <sys/stat.h>

#pragma warning(disable : 4512)

#define WM_INPUTCHANGED (WM_USER + 0x3000)
#define WM_FOCUSTEXTBOX (WM_USER + 0x3001)

typedef BOOL (__cdecl *ENUMPROFILECALLBACK) (wchar_t *tszFullPath, wchar_t *profile, LPARAM lParam);

/////////////////////////////////////////////////////////////////////////////////////////
// Profile creator

static int findProfiles(wchar_t *szProfileDir, ENUMPROFILECALLBACK callback, LPARAM lParam)
{
	// find in Miranda NG profile subfolders
	wchar_t searchspec[MAX_PATH];
	mir_snwprintf(searchspec, L"%s\\*.*", szProfileDir);
	
	WIN32_FIND_DATA ffd;
	HANDLE hFind = FindFirstFile(searchspec, &ffd);
	if (hFind == INVALID_HANDLE_VALUE)
		return 0;

	do {
		// find all subfolders except "." and ".."
		if ((ffd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) && mir_wstrcmp(ffd.cFileName, L".") && mir_wstrcmp(ffd.cFileName, L"..")) {
			wchar_t buf[MAX_PATH], profile[MAX_PATH];
			mir_snwprintf(buf, L"%s\\%s\\%s.dat", szProfileDir, ffd.cFileName, ffd.cFileName);
			if (_waccess(buf, 0) == 0) {
				mir_snwprintf(profile, L"%s.dat", ffd.cFileName);
				if (!callback(buf, profile, lParam))
					break;
			}
		}
	}
		while (FindNextFile(hFind, &ffd));

	FindClose(hFind);
	return 1;
}

static LRESULT CALLBACK ProfileNameValidate(HWND edit, UINT msg, WPARAM wParam, LPARAM lParam)
{
	if (msg == WM_CHAR) {
		if (wcschr(L".?/\\#' ", (wchar_t)wParam) != nullptr)
			return 0;
		PostMessage(GetParent(edit), WM_INPUTCHANGED, 0, 0);
	}
	return mir_callNextSubclass(edit, ProfileNameValidate, msg, wParam, lParam);
}

class CCreateProfileDlg : public CDlgBase
{
	CCtrlButton &m_btnOk;
	PROFILEMANAGERDATA *m_pd;

	int CreateProfile(wchar_t *profile, DATABASELINK *link)
	{
		wchar_t buf[256];
		int err = 0;
		// check if the file already exists
		wchar_t *file = wcsrchr(profile, '\\');
		if (file) file++;
		if (_waccess(profile, 0) == 0) {
			// file already exists!
			mir_snwprintf(buf,
				TranslateT("The profile '%s' already exists. Do you want to move it to the Recycle Bin?\n\nWARNING: The profile will be deleted if Recycle Bin is disabled.\nWARNING: A profile may contain confidential information and should be properly deleted."),
				file);
			if (MessageBox(m_hwnd, buf, TranslateT("The profile already exists"), MB_ICONQUESTION | MB_YESNO | MB_DEFBUTTON2) != IDYES)
				return 0;

			// move the file
			SHFILEOPSTRUCT sf = {};
			sf.wFunc = FO_DELETE;
			sf.pFrom = buf;
			sf.fFlags = FOF_NOCONFIRMATION | FOF_NOERRORUI | FOF_SILENT | FOF_ALLOWUNDO;
			mir_snwprintf(buf, L"%s\0", profile);
			if (SHFileOperation(&sf) != 0) {
				mir_snwprintf(buf, TranslateT("Couldn't move '%s' to the Recycle Bin. Please select another profile name."), file);
				MessageBox(m_hwnd, buf, TranslateT("Problem moving profile"), MB_ICONINFORMATION | MB_OK);
				return 0;
			}
			// now the file should be gone!
		}
		// ask the database to create the profile
		CreatePathToFileW(profile);
		if ((err = link->makeDatabase(profile)) != ERROR_SUCCESS) {
			mir_snwprintf(buf, TranslateT("Unable to create the profile '%s', the error was %x"), file, err);
			MessageBox(m_hwnd, buf, TranslateT("Problem creating profile"), MB_ICONERROR | MB_OK);
			return 0;
		}

		// the profile has been created!
		g_bDbCreated = true;
		return 1;
	}

	bool       m_bFocused;
	CCtrlCombo m_driverList;
	CCtrlEdit  m_profileName;
	CCtrlBase  m_warning;

public:
	CCreateProfileDlg(CCtrlButton &_btn, PROFILEMANAGERDATA *_pd) :
		CDlgBase(g_hInst, IDD_PROFILE_NEW),
		m_btnOk(_btn),
		m_pd(_pd),
		m_bFocused(false),
		m_driverList(this, IDC_PROFILEDRIVERS),
		m_profileName(this, IDC_PROFILENAME),
		m_warning(this, IDC_NODBDRIVERS)
	{}

	virtual void OnInitDialog()
	{
		// what, no plugins?!
		if (arDbPlugins.getCount() == 0) {
			m_driverList.Enable(false);
			m_profileName.Enable(false);
			ShowWindow(m_warning.GetHwnd(), TRUE);
		}
		else {
			for (auto &p : arDbPlugins)
				m_driverList.AddString(TranslateW(p->szFullName), (LPARAM)p);
		}

		// default item
		m_driverList.SetCurSel(0);

		// subclass the profile name box
		mir_subclassWindow(m_profileName.GetHwnd(), ProfileNameValidate);

		// decide if there is a default profile name given in the INI and if it should be used
		if (m_pd->noProfiles || (shouldAutoCreate(m_pd->ptszProfile) && _waccess(m_pd->ptszProfile, 0))) {
			wchar_t *profile = wcsrchr(m_pd->ptszProfile, '\\');
			if (profile) ++profile;
			else profile = m_pd->ptszProfile;

			wchar_t *p = wcsrchr(profile, '.');
			wchar_t c = 0;
			if (p) { c = *p; *p = 0; }

			m_profileName.SetText(profile);
			if (c) *p = c;
		}

		// focus on the textbox
		PostMessage(m_hwnd, WM_FOCUSTEXTBOX, 0, 0);
	}

	virtual INT_PTR DlgProc(UINT msg, WPARAM wParam, LPARAM lParam)
	{
		switch (msg) {
		case WM_FOCUSTEXTBOX:
			SetFocus(m_profileName.GetHwnd());
			break;

		case WM_INPUTCHANGED: // when input in the edit box changes
			NotifyChange();
			m_btnOk.Enable(GetWindowTextLength(m_profileName.GetHwnd()) > 0);
			break;

		case WM_SHOWWINDOW:
			if (wParam) {
				m_btnOk.SetText(TranslateT("&Create"));
				SendMessage(m_hwnd, WM_INPUTCHANGED, 0, 0);
				m_bFocused = true;
			}
			else m_bFocused = false;
			break;
		}
		return CDlgBase::DlgProc(msg, wParam, lParam);
	}

	virtual void OnApply()
	{
		LRESULT curSel = m_driverList.GetCurSel();
		if (curSel == -1 || !m_bFocused)
			return; // should never happen

		ptrW szName(m_profileName.GetText());
		if (mir_wstrlen(szName) == 0)
			return;

		// profile placed in "profile_name" subfolder
		mir_snwprintf(m_pd->ptszProfile, MAX_PATH, L"%s\\%s\\%s.dat", m_pd->ptszProfileDir, szName, szName);
		m_pd->newProfile = 1;
		m_pd->dblink = (DATABASELINK *)m_driverList.GetItemData(curSel);

		if (CreateProfile(m_pd->ptszProfile, m_pd->dblink) == 0)
			SetWindowLongPtr(m_hwnd, DWLP_MSGRESULT, PSNRET_INVALID_NOCHANGEPAGE);
		else
			m_pd->bRun = true;
	}
};

/////////////////////////////////////////////////////////////////////////////////////////
// Profile selector

class CChooseProfileDlg : public CDlgBase
{
	CCtrlButton &m_btnOk;
	PROFILEMANAGERDATA *m_pd;
	HANDLE m_hFileNotify;

	struct ProfileEnumData
	{
		ProfileEnumData(CCtrlListView &_list, wchar_t *_profile) :
			list(_list),
			szProfile(_profile)
		{}

		CCtrlListView &list;
		wchar_t* szProfile;
	};

	static BOOL EnumProfilesForList(wchar_t *tszFullPath, wchar_t *profile, LPARAM lParam)
	{
		ProfileEnumData *ped = (ProfileEnumData*)lParam;
		CCtrlListView &list = ped->list;

		wchar_t sizeBuf[64];
		bool bFileLocked = true;

		wchar_t *p = wcsrchr(profile, '.');
		mir_wstrcpy(sizeBuf, L"0 KB");
		if (p != nullptr) *p = 0;

		LVITEM item = { 0 };
		item.mask = LVIF_TEXT | LVIF_IMAGE;
		item.pszText = profile;
		item.iItem = 0;

		struct _stat statbuf;
		if (_wstat(tszFullPath, &statbuf) == 0) {
			if (statbuf.st_size > 1000000) {
				mir_snwprintf(sizeBuf, L"%.3lf", (double)statbuf.st_size / 1048576.0);
				mir_wstrcpy(sizeBuf + 5, L" MB");
			}
			else {
				mir_snwprintf(sizeBuf, L"%.3lf", (double)statbuf.st_size / 1024.0);
				mir_wstrcpy(sizeBuf + 5, L" KB");
			}
			bFileLocked = !fileExist(tszFullPath);
		}

		DATABASELINK *dblink;
		switch (touchDatabase(tszFullPath, &dblink)) {
		case ERROR_SUCCESS:
			item.iImage = bFileLocked;
			break;

		case EGROKPRF_OBSOLETE:
			item.iImage = 2;
			break;

		default:
			item.iImage = 3;
		}

		int iItem = list.InsertItem(&item);
		if (mir_wstrcmpi(ped->szProfile, tszFullPath) == 0)
			list.SetItemState(iItem, LVIS_SELECTED | LVIS_FOCUSED, LVIS_SELECTED | LVIS_FOCUSED);

		list.SetItemText(iItem, 2, sizeBuf);

		if (dblink != nullptr) {
			if (bFileLocked) // file locked
				list.SetItemText(iItem, 1, TranslateT("<In use>"));
			else
				list.SetItemText(iItem, 1, TranslateW(dblink->szFullName));
		}
		else list.SetItemText(iItem, 1, TranslateT("<Unknown format>"));

		return TRUE;
	}

	void DeleteProfile(int iItem)
	{
		if (iItem < 0)
			return;

		wchar_t profile[MAX_PATH], profilef[MAX_PATH * 2];

		LVITEM item = { 0 };
		item.mask = LVIF_TEXT;
		item.iItem = iItem;
		item.pszText = profile;
		item.cchTextMax = _countof(profile);
		if (!m_profileList.GetItem(&item))
			return;

		mir_snwprintf(profilef, TranslateT("Are you sure you want to remove profile \"%s\"?"), profile);
		if (IDYES != MessageBox(nullptr, profilef, L"Miranda NG", MB_YESNO | MB_TASKMODAL | MB_ICONWARNING))
			return;

		mir_snwprintf(profilef, L"%s\\%s%c", m_pd->ptszProfileDir, profile, 0);

		SHFILEOPSTRUCT sf = {};
		sf.wFunc = FO_DELETE;
		sf.pFrom = profilef;
		sf.fFlags = FOF_NOCONFIRMATION | FOF_SILENT | FOF_ALLOWUNDO;
		SHFileOperation(&sf);
		m_profileList.DeleteItem(item.iItem);
	}

	void CheckRun()
	{
		m_btnOk.Enable(m_profileList.GetSelectedCount() == 1);

		wchar_t profile[MAX_PATH];
		LVITEM item = { 0 };
		item.mask = LVIF_TEXT | LVIF_IMAGE;
		item.iItem = m_profileList.GetNextItem(-1, LVNI_SELECTED | LVNI_ALL);
		item.pszText = profile;
		item.cchTextMax = _countof(profile);
		if (!m_profileList.GetItem(&item))
			return;

		switch(item.iImage) {
		case 3:
			m_btnOk.Enable(false);
			return;

		case 2:
			m_btnOk.SetText(TranslateT("&Convert"));
			m_pd->bRun = false;
			break;

		default:
			m_btnOk.SetText(TranslateT("&Run"));
			m_pd->bRun = true;
		}

		// profile is placed in "profile_name" subfolder

		wchar_t tmpPath[MAX_PATH];
		mir_snwprintf(tmpPath, L"%s\\%s.dat", m_pd->ptszProfileDir, profile);
		if (_waccess(tmpPath, 2))
			mir_snwprintf(m_pd->ptszProfile, MAX_PATH, L"%s\\%s\\%s.dat", m_pd->ptszProfileDir, profile, profile);
		else
			wcsncpy_s(m_pd->ptszProfile, MAX_PATH, tmpPath, _TRUNCATE);
	}

	void ExecuteMenu(LPARAM lParam)
	{
		LVHITTESTINFO lvht = { 0 };
		lvht.pt.x = GET_X_LPARAM(lParam);
		lvht.pt.y = GET_Y_LPARAM(lParam);
		ScreenToClient(m_profileList.GetHwnd(), &lvht.pt);

		if (m_profileList.HitTest(&lvht) == -1)
			return;

		if (lvht.iItem == -1)
			return;

		LVITEM tvi = { 0 };
		tvi.mask = LVIF_IMAGE;
		tvi.iItem = lvht.iItem;
		if (!m_profileList.GetItem(&tvi))
			return;

		lvht.pt.x = GET_X_LPARAM(lParam);
		lvht.pt.y = GET_Y_LPARAM(lParam);

		HMENU hMenu = CreatePopupMenu();
		if (tvi.iImage < 2) {
			AppendMenu(hMenu, MF_STRING, 1, TranslateT("Run"));
			AppendMenu(hMenu, MF_SEPARATOR, 0, nullptr);
		}

		AppendMenu(hMenu, MF_STRING, 2, TranslateT("Delete"));
		int index = TrackPopupMenu(hMenu, TPM_RETURNCMD, lvht.pt.x, lvht.pt.y, 0, m_hwnd, nullptr);
		switch (index) {
		case 1:
			SendMessage(GetParent(m_hwndParent), WM_COMMAND, IDOK, 0);
			break;

		case 2:
			DeleteProfile(lvht.iItem);
			break;
		}
		DestroyMenu(hMenu);
	}

	CCtrlListView m_profileList;

public:
	CChooseProfileDlg(CCtrlButton &_btn, PROFILEMANAGERDATA *_pd) :
		CDlgBase(g_hInst, IDD_PROFILE_SELECTION),
		m_btnOk(_btn),
		m_pd(_pd),
		m_profileList(this, IDC_PROFILELIST)
	{
		m_profileList.OnItemChanged = Callback(this, &CChooseProfileDlg::list_OnItemChanged);
		m_profileList.OnKeyDown = Callback(this, &CChooseProfileDlg::list_OnKeyDown);
		m_profileList.OnGetInfoTip = Callback(this, &CChooseProfileDlg::list_OnGetTip);
		m_profileList.OnDoubleClick = Callback(this, &CChooseProfileDlg::list_OnDblClick);
	}

	virtual void OnInitDialog()
	{
		// set columns
		LVCOLUMN col;
		col.mask = LVCF_TEXT | LVCF_WIDTH;
		col.pszText = TranslateT("Profile");
		col.cx = 100;
		m_profileList.InsertColumn(0, &col);

		col.pszText = TranslateT("Driver");
		col.cx = 150 - GetSystemMetrics(SM_CXVSCROLL);
		m_profileList.InsertColumn(1, &col);

		col.pszText = TranslateT("Size");
		col.cx = 60;
		m_profileList.InsertColumn(2, &col);

		// icons
		HIMAGELIST hImgList = ImageList_Create(16, 16, ILC_MASK | ILC_COLOR32, 2, 1);
		ImageList_AddIcon_NotShared(hImgList, MAKEINTRESOURCE(IDI_USERDETAILS));
		ImageList_AddIcon_NotShared(hImgList, MAKEINTRESOURCE(IDI_DELETE));
		ImageList_AddIcon_NotShared(hImgList, MAKEINTRESOURCE(IDI_MWARNING));
		ImageList_AddIcon_NotShared(hImgList, MAKEINTRESOURCE(IDI_MFATAL));

		// LV will destroy the image list
		m_profileList.SetImageList(hImgList, LVSIL_SMALL);
		m_profileList.SetExtendedListViewStyle(m_profileList.GetExtendedListViewStyle() | LVS_EX_DOUBLEBUFFER | LVS_EX_INFOTIP | LVS_EX_LABELTIP | LVS_EX_FULLROWSELECT);

		// find all the profiles
		ProfileEnumData ped(m_profileList, m_pd->ptszProfile);
		findProfiles(m_pd->ptszProfileDir, EnumProfilesForList, (LPARAM)&ped);
		PostMessage(m_hwnd, WM_FOCUSTEXTBOX, 0, 0);

		m_hFileNotify = FindFirstChangeNotification(m_pd->ptszProfileDir, TRUE, FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_LAST_WRITE);
		if (m_hFileNotify != INVALID_HANDLE_VALUE)
			SetTimer(m_hwnd, 0, 1200, nullptr);
	}

	virtual void OnDestroy()
	{
		KillTimer(m_hwnd, 0);
		FindCloseChangeNotification(m_hFileNotify);
	}

	void list_OnItemChanged(CCtrlListView::TEventInfo*)
	{
		CheckRun();
	}

	void list_OnKeyDown(CCtrlListView::TEventInfo *evt)
	{
		if (evt->nmlvkey->wVKey == VK_DELETE)
			DeleteProfile(m_profileList.GetNextItem(-1, LVNI_SELECTED | LVNI_ALL));
	}

	void list_OnGetTip(CCtrlListView::TEventInfo *evt)
	{
		if (auto pTip = evt->nmlvit) {
			wchar_t profilename[MAX_PATH], tszFullPath[MAX_PATH];
			struct _stat statbuf;
			m_profileList.GetItemText(pTip->iItem, 0, profilename, _countof(profilename));
			mir_snwprintf(tszFullPath, L"%s\\%s\\%s.dat", m_pd->ptszProfileDir, profilename, profilename);
			_wstat(tszFullPath, &statbuf);
			mir_snwprintf(pTip->pszText, pTip->cchTextMax, L"%s\n%s: %s\n%s: %s", tszFullPath, TranslateT("Created"), rtrimw(NEWWSTR_ALLOCA(_wctime(&statbuf.st_ctime))), TranslateT("Modified"), rtrimw(NEWWSTR_ALLOCA(_wctime(&statbuf.st_mtime))));
		}
	}

	void list_OnDblClick(CCtrlListView::TEventInfo*)
	{
		CheckRun();
		EndDialog(GetParent(m_hwndParent), 1);
	}

	virtual INT_PTR DlgProc(UINT msg, WPARAM wParam, LPARAM lParam)
	{
		switch (msg) {
		case WM_TIMER:
			if (WaitForSingleObject(m_hFileNotify, 0) == WAIT_OBJECT_0) {
				m_profileList.DeleteAllItems();
				ProfileEnumData ped(m_profileList, m_pd->ptszProfile);
				findProfiles(m_pd->ptszProfileDir, EnumProfilesForList, (LPARAM)&ped);
				FindNextChangeNotification(m_hFileNotify);
			}
			break;

		case WM_FOCUSTEXTBOX:
			SetFocus(m_profileList.GetHwnd());
			if (m_pd->ptszProfile[0] == 0 || m_profileList.GetSelectedCount() == 0)
				m_profileList.SetItemState(0, LVIS_SELECTED | LVIS_FOCUSED, LVIS_SELECTED | LVIS_FOCUSED);
			break;

		case WM_SHOWWINDOW:
			if (wParam)
				CheckRun();
			break;

		case WM_CONTEXTMENU:
			ExecuteMenu(lParam);
			break;
		}

		return CDlgBase::DlgProc(msg, wParam, lParam);
	}
};

/////////////////////////////////////////////////////////////////////////////////////////
// Tab manager + its envelope

class CProfileManager : public CDlgBase
{
	PROFILEMANAGERDATA *m_pd;

	CCtrlPages  m_tab;
	CCtrlButton m_btnOk;
	CCtrlCombo  m_servicePlugs;
	CCtrlBase   m_warning;

public:
	CProfileManager(PROFILEMANAGERDATA *_pd) :
		CDlgBase(g_hInst, IDD_PROFILEMANAGER),
		m_btnOk(this, IDOK),
		m_pd(_pd),
		m_tab(this, IDC_TABS),
		m_servicePlugs(this, IDC_SM_COMBO),
		m_warning(this, IDC_SM_LABEL)
	{
		m_btnOk.OnClick = Callback(this, &CProfileManager::onOk);

		m_tab.AddPage(LPGENW("My profiles"), nullptr, new CChooseProfileDlg(m_btnOk, m_pd));
		m_tab.AddPage(LPGENW("New profile"), nullptr, new CCreateProfileDlg(m_btnOk, m_pd));
	}
	
	virtual void OnInitDialog()
	{
		// MUST NOT be replaced with Window_SetIcon_IcoLib!!!
		SendMessage(m_hwnd, WM_SETICON, ICON_SMALL, (LPARAM)LoadImage(g_hInst, MAKEINTRESOURCE(IDI_DETAILSLOGO), IMAGE_ICON, g_iIconSX, g_iIconSY, 0));
		SendMessage(m_hwnd, WM_SETICON, ICON_BIG, (LPARAM)LoadImage(g_hInst, MAKEINTRESOURCE(IDI_DETAILSLOGO), IMAGE_ICON, g_iIconX, g_iIconY, 0));

		if (m_pd->noProfiles || shouldAutoCreate(m_pd->ptszProfile))
			m_tab.ActivatePage(1);

		// service mode combobox
		if (servicePlugins.getCount() == 0) {
			ShowWindow(m_warning.GetHwnd(), FALSE);
			ShowWindow(m_servicePlugs.GetHwnd(), FALSE);
		}
		else {
			m_servicePlugs.AddStringA("", -1);
			m_servicePlugs.SetCurSel(0);

			for (int i = 0; i < servicePlugins.getCount(); i++) {
				pluginEntry *p = servicePlugins[i];
				m_servicePlugs.AddString(TranslateW(p->pluginname), i);
			}
		}
	}

	virtual void OnDestroy()
	{
		LRESULT curSel = m_servicePlugs.GetCurSel();
		if (curSel != -1) {
			int idx = m_servicePlugs.GetItemData(curSel);
			if (idx != -1)
				plugin_service = servicePlugins[idx];
		}

		DestroyIcon((HICON)SendMessage(m_hwnd, WM_SETICON, ICON_SMALL, 0));
		DestroyIcon((HICON)SendMessage(m_hwnd, WM_SETICON, ICON_BIG, 0));
	}

	void onOk(CCtrlButton*)
	{
		EndDialog(m_hwnd, 1);
	}
};

int getProfileManager(PROFILEMANAGERDATA *pd)
{
	return CProfileManager(pd).DoModal();
}